-
Notifications
You must be signed in to change notification settings - Fork 72
[SYNPY-1589] Implement core functionality of Evaluation OOP model #1245
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
base: synpy-1589-evaluation-model-main
Are you sure you want to change the base?
[SYNPY-1589] Implement core functionality of Evaluation OOP model #1245
Conversation
…ividually. test improvements.
…he class object itself rather than feeding it into the method. new to_synapse_request
|
||
return self | ||
|
||
def to_synapse_request(self, request_type: 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'd like people's thoughts on this re-design:
Following the bug fix for update_async
, I realized the way I was having users update/create their evaluations didn't match, which was confusing.
The original logic looked like this:
# Creating my first evaluation
evaluation = Evaluation(name="eval_name", description=...)
await evaluation.store_async(synapse_client=syn)
# Updating my evaluation
await evaluation.update_async(name="new_name", synapse_client=syn)
So for store_async
, the logic relied on the object attributes to build and submit the request. For update_async
, the logic relied on the new attributes being passed through as parameters in the method call.
I decided to consolidate this inconsistency so both update
and store
functionality relies on the user updating their evaluation object accordingly.
This means neither update_async
and store_async
require any parameters get passed to the methods themselves, rather the object itself needs to be updated. This will also help the user know what exactly their evaluation is going to look like when they make an update or store a new evaluation.
This is what it would look like irl:
In [27]: await new_evaluation.store_async(synapse_client=syn)
Out[27]: Evaluation(id='9616331', etag='8a8dbe71-5ce9-45d2-95f2-7f80987db2f7', name='ah2', description='ah', owner_id='3489628', created_on='2025-09-16T15:09:58.819Z', content_source='syn69835628', submission_instructions_message='ah', submission_receipt_message='ah')
In [28]: new_evaluation.name = 'ah2_updated'
In [29]: await new_evaluation.update_async(synapse_client=syn)
Out[29]: Evaluation(id='9616331', etag='8c027b5b-6447-4009-ba12-4a33a4d0b363', name='ah2_updated', description='ah', owner_id='3489628', created_on='2025-09-16T15:09:58.819Z', content_source='syn69835628', submission_instructions_message='ah', submission_receipt_message='ah')
The downside of this is a user would have to manually update every attribute e.g:
test_evaluation.name = new_name
test_evaluation.description = new_description
test_evaluation.submission_instructions_message = new_instructions
LMK your thoughts @BryanFauble @thomasyu888
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 have a strong dislike how you've split apart the create and update logic, however - If it ends up the case that these are split methods a user needs to call I would suggest that they are renamed to create_async
and update_async
.
However, here is something to consider. There is an alternative way to go about this which would allow you to continue using store_async
for create OR update operations. If this is the route you go this is how I accomplished this for other classes.
synapsePythonClient/synapseclient/models/project.py
Lines 335 to 349 in da36c84
if ( | |
self.create_or_update | |
and not self._last_persistent_instance | |
and ( | |
existing_project_id := await get_id( | |
entity=self, synapse_client=synapse_client, failure_strategy=None | |
) | |
) | |
and ( | |
existing_project := await Project(id=existing_project_id).get_async( | |
synapse_client=synapse_client | |
) | |
) | |
): | |
merge_dataclass_entities(source=existing_project, destination=self) |
To explain what is going on here:
- The
create_or_update
is optional here, and is probably fine not to have for Evaluation. - The
_last_persistent_instance
is a bit of a complex concept the purpose behind this was to track if the class instance had already been read from Synapse, or stored to Synapse before during the current session. In most cases it would end up being None, but it would be filled in during theget_async
operation since it was the last state that the client application knew the data to be. - There is a search to find the existing entity id (Evaluation ID in your case). This would either get the ID attribute if passed in OR search for it by name.
- If an ID is found then a new
get_async
operation is performed to get the current state that Synapse has. After this is performed then we continue to the merge:
The merge is the important piece here, and what I think is the core problem that you're trying to solve. This merge operation allows you to perform non-destructive updates, if you implement it this would mean you could do something like:
my_eval = Evaluation(id='1234')
my_eval.description = new_description
my_eval.store()
The logic I described above would mean that ONLY the description gets updated here in a non-destructive update.
An edge case and something you might be thinking here, is "How do I delete fields/set them to None?" and that would be a great question.
The check for _last_persistent_instance
means that after a resource has been read from Synapse (Or previously stored) any NEW store()
operations are total replacements of the object.
That means this behaves the same as the small example I gave above:
my_eval = Evaluation(id='1234').get()
my_eval.description = new_description
my_eval.store()
But then you could also do something like:
my_eval = Evaluation(id='1234')
my_eval.description = None # Delete description
my_eval.store()
Hopefully this makes sense.
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.
@BryanFauble thanks for the feedback - since this is the logic already in place for existing models, it makes sense for me to implement it this way too for consistency. I'm working on the refactor now and will tag you when it's finished.
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.
@BryanFauble I've implemented the refactor in this commit: ecfc2d2
Notable details:
- I refactored and implemented
merge_dataclass_entities
to also support Evaluation objects, not just Synapse entities. Another major change was adding a new argument that passes a list of fields that SHOULD NOT change from the source object if a user changes them, such as things likeowner_id
. I'm open to discussing if this is worth it or not. The motivation behind this was to prevent users from "reusing" the Evaluation objects to interact with other Evaluation objects. - I implemented those attributes you leverage for when/when not to trigger
merge_dataclass_entities
, such as_last_persistent_instance
andhas_changed
. - One area where I deviated from your design in the Project model is I didn't implement
create_or_update
. Instead, I relied on theid
and_last_persistent_instance
andhas_changed
models for when and when not to make a non-destructive change to the Synapse object. Because this is a choice that's happening "behind the curtain", I opted for adding a stdout message notifying the user when their object is going down the "create" or "update" pipeline, or not being updated at all.
Let me know your thoughts on this implementation, and if you have any other suggestions for the store_async
portion of the model!
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.
An edge case and something you might be thinking here, is "How do I delete fields/set them to None?"
All of the fields for the Evaluation model are part of the request JSON, so they have to be not-None
, which was another motivation for including the new parameter fields_to_preserve_from_source
in the merge utility function. If a user sets any of those specified fields to None
, they should get replaced with those values which were retrieved from the last communication with Synapse so we can create a proper request JSON dictionary for the following communication.
…able field. style
See #1244