-
Notifications
You must be signed in to change notification settings - Fork 179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(robot_server): add system/time GET & PUT endpoints #6403
Conversation
async def get_time() -> time_models.SystemTimeResponse: | ||
try: | ||
res = await time.get_system_time() | ||
except Exception as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should catch/raise a specific exception or remove this.
try: | ||
time_dt, res_status = await time.set_system_time( | ||
new_time.data.attributes.systemTime) | ||
except Exception as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above regarding exception
api/src/opentrons/system/time.py
Outdated
res_dict = {} | ||
for line in lines: | ||
if line: | ||
res_dict[line.split('=')[0]] = line.split('=')[1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is wasteful to split the line twice here. I recommend you have a temp var for the result of line.split('=')
api/src/opentrons/system/time.py
Outdated
log = logging.getLogger(__name__) | ||
|
||
|
||
def _str_to_dict(res_str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any tests for this function. Is every line guaranteed to have an =
in it?
api/src/opentrons/system/time.py
Outdated
|
||
def _str_to_dict(res_str): | ||
lines = res_str.split('\n') | ||
res_dict = {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps instead of returning a Dict[str, str]
you can return a Dataclass?
At least it would be nice to convert "yes/no" values to bools.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Converting the string items to Dataclass objects seemed a bit too cumbersome esp when the objects would be used just once in the code. (But if you think that's not so then I'm open to suggestions on how to do the conversions)
Converted the the dict yes/no items to bools.
api/src/opentrons/system/time.py
Outdated
return _str_to_dict(out.decode()) | ||
|
||
|
||
async def _set_time(time: str, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is caller to know what the values in the Tuple mean?
api/src/opentrons/system/time.py
Outdated
:return: Tuple specifying current date read and error message, if any. | ||
""" | ||
status = await _time_status(loop) | ||
if status['LocalRTC'] == "yes" or status['NTPSynchronized'] == "yes": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice if these were True/False instead of magic "yes" string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It kinda seems like opentrons.system.time
should be robot_server.(something).time
. None of its functions are called from the rest of the opentrons
module, and it isn't really related to robot operation. I know there's other system components in the opentrons
package, but that's just because the robot_server
package didn't exist when they were written, and I think we should move them as well (not necessarily in this pr though)
api/src/opentrons/system/time.py
Outdated
return out.decode(), err.decode() | ||
|
||
|
||
async def get_system_time(loop: asyncio.AbstractEventLoop = None) -> datetime: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is going to be called pretty frequently so the app can make sure that the time stays in sync, right? I don't think we should be shelling out for something like that, or that frequently. datetime.datetime.now()
should work a lot better with just a single syscall and file access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No really. That was indeed what we had drafted, but after giving it some thought, we decided to update system time only when user initiates connection to the robot (reason being that the system clock stays correct after it's once set).
Although, that does not take into account any time getters in order to show robot time in the app, if we decide to do that. In which case, you are right, we'll need to switch to datetime.now()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thought about it a bit more, you're right, even if we won't be calling the function frequently right now, there's no harm in making the switch to datetime.now()
. That way we won't have make the change later.
api/src/opentrons/system/time.py
Outdated
now = await get_system_time(loop) | ||
# TODO: check with team whether to return whole error or | ||
# just an error status | ||
return now, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can definitely log the error message if it exists at error
level (unless that gets too spammy)
api/src/opentrons/system/time.py
Outdated
new_time_str = new_time_dt.strftime("%Y-%m-%d %H:%M:%S") | ||
log.info(f'Setting time to {new_time_str} UTC') | ||
out, err = await _set_time(new_time_str) | ||
log.info(f'{out if out else err}') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a little spammy
|
||
class SystemTimeAttributes(BaseModel): | ||
systemTime: datetime | ||
setTimeError: Optional[str] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we do this or should we use an error response?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Changed to error response.
037342d
to
ba52ebc
Compare
Agreed about moving system components out of opentrons module and into robot_server. Since there's other components that need to be moved too, I'll put up a different PR to move all of them at once. |
…me comments -added router tests, added response links -raise error instead of returning error in regular response
2484cbf
to
6526c88
Compare
Codecov Report
@@ Coverage Diff @@
## edge #6403 +/- ##
=======================================
Coverage ? 80.25%
=======================================
Files ? 230
Lines ? 19934
Branches ? 0
=======================================
Hits ? 15999
Misses ? 3935
Partials ? 0
Continue to review full report at Codecov.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits - I think some of the server tests should actually be tavern integration tests - but also I really think that opentrons.system.time
should really be robot_server.system.time
. There's a couple reasons for this:
- The point of the robot server is to move the stuff that's unrelated to the OT-2 as a robot and protocol execution system out of the
opentrons
package. We already pulled out all the HTTP stuff and the RPC protocol stuff; the lines get blurry around the orchestration components that run protocols on the robot, but we're in the process of pulling those out too. Things like management oftimesyncd
(and for that matter network connectivity) really have no place in theopentrons
package conceptually. - Pulling it into
robot_server
would let you significantly clean up its interface around errors. You can't use the nice server exceptions in theopentrons
package because theopentrons
package can't rely onrobot_server
, so your options are to raise some generic exception that will later be caught and reraised, or do this thing where you return the error string. Neither is a great choice - the first is just confusing, and the second makesrobot_server
(and theservice
subpackage at that!) have to understand the output oftimesyncd
, breaking the encapsulation ofopentrons.system.time
. If thetime
package is inrobot_server
, it can do all its own parsing and raise the appropriate exception (if necessary) directly.
api/src/opentrons/system/time.py
Outdated
prop, val = line.split('=') | ||
res_dict[prop] = val if val not in ['yes', 'no'] \ | ||
else val == 'yes' # Convert yes/no to boolean value | ||
except (ValueError, IndexError) as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit]: the most probably failure case here is that we didn't anticipate a particular kind of output from timedatectl
, but the output we did anticipate is the same. given that, should we maybe do this kind of checking on a line-by-line basis and not raise the exception? instead, we can log with a specific tag and look for it on log monitoring.
sys_time, err = await time.set_system_time( | ||
new_time.data.attributes.systemTime) | ||
if err: | ||
if 'already synchronized with NTP or RTC' in err: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me, this is the wrong place to do this. It leaks implementation details - the robot server endpoint function now has to care about the exact details of how timesyncd
formats error messages.
assert response.status_code == 500 | ||
|
||
|
||
def test_get_system_time(api_client, mock_system_time, response_links): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably wants to be a tavern integration test
assert response.status_code == 200 | ||
|
||
|
||
def test_set_with_missing_field(api_client, mock_system_time): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably wants to be a tavern integration test
assert response.status_code == 422 | ||
|
||
|
||
def test_set_system_time(api_client, mock_system_time, response_links): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this probably wants to be a tavern integration test, but given the degree of mocking maybe it wouldn't be worth it.
@sfoster1 Made all the changes requested. Talked with Amit offline about tavern tests and we decided to only add those tests that don't require mocking the system components. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
async def get_time() -> time_models.SystemTimeResponse: | ||
res = await time.get_system_time() | ||
return _create_response(res) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This warning is not quite accurate. We need to configure tests to recognize coverage of tavern tests. I will make a ticket now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a really cool feature to display it inline though!
res_dict[prop] = val if val not in ['yes', 'no'] \ | ||
else val == 'yes' # Convert yes/no to boolean value | ||
except (ValueError, IndexError) as e: | ||
log.error("Error converting timedatectl string: {}".format(e)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth logging the actual line contents too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! Love it being integrated into the robot server
Overview
This PR adds GET & PUT time endpoints to robot server. These will be used by the RunApp to get/ update robot's date & time. This will enable us to update robot's time in absence of an RTC/ internet access.
Addresses #3872
Changelog
system
service torobot_server
/system
time
component toopentrons/system
Review requests
Risk assessment
Low. The endpoints are not being used by anything right now & the code is isolated.