Skip to content

Conversation

@jaymedina
Copy link
Contributor

@jaymedina jaymedina commented Sep 30, 2025

See #1250

All tests passing:

(synapseclient) bash-3.2$ pytest tests/integration/synapseclient/models/synchronous/*submission*.py 
======================================================================= test session starts ========================================================================
platform darwin -- Python 3.9.18, pytest-8.3.3, pluggy-1.5.0
rootdir: /Users/jmedina/Documents/forks/synapsePythonClient
configfile: pytest.ini
plugins: asyncio-0.24.0, socket-0.7.0, anyio-4.6.0, mock-3.14.0
asyncio: mode=auto, default_loop_scope=session
collected 67 items                                                                                                                                                 

tests/integration/synapseclient/models/synchronous/test_submission.py .....................                                                                  [ 31%]
tests/integration/synapseclient/models/synchronous/test_submission_bundle.py ...............                                                                 [ 53%]
tests/integration/synapseclient/models/synchronous/test_submission_status.py .......................                                                         [ 88%]
tests/integration/synapseclient/models/synchronous/test_submissionview.py ........                                                                           [100%]
======================================================================= test session starts ========================================================================
platform darwin -- Python 3.9.18, pytest-8.3.3, pluggy-1.5.0
rootdir: /Users/jmedina/Documents/forks/synapsePythonClient
configfile: pytest.ini
plugins: asyncio-0.24.0, socket-0.7.0, anyio-4.6.0, mock-3.14.0
asyncio: mode=auto, default_loop_scope=session
collected 68 items                                                                                                                                                 

tests/integration/synapseclient/models/async/test_submission_async.py ......................                                                                 [ 32%]
tests/integration/synapseclient/models/async/test_submission_bundle_async.py ...............                                                                 [ 54%]
tests/integration/synapseclient/models/async/test_submission_status_async.py .......................                                                         [ 88%]
======================================================================= test session starts ========================================================================
platform darwin -- Python 3.9.18, pytest-8.3.3, pluggy-1.5.0
rootdir: /Users/jmedina/Documents/forks/synapsePythonClient
configfile: pytest.ini
plugins: asyncio-0.24.0, socket-0.7.0, anyio-4.6.0, mock-3.14.0
asyncio: mode=auto, default_loop_scope=session
collected 53 items                                                                                                                                                 

tests/unit/synapseclient/models/async/unit_test_submission_async.py ................                                                                         [ 30%]
tests/unit/synapseclient/models/async/unit_test_submission_bundle_async.py ..............                                                                    [ 56%]
tests/unit/synapseclient/models/async/unit_test_submission_status_async.py .......................                                                           [100%]

======================================================================== 53 passed in 1.61s ========================================================================
======================================================================= test session starts ========================================================================
platform darwin -- Python 3.9.18, pytest-8.3.3, pluggy-1.5.0
rootdir: /Users/jmedina/Documents/forks/synapsePythonClient
configfile: pytest.ini
plugins: asyncio-0.24.0, socket-0.7.0, anyio-4.6.0, mock-3.14.0
asyncio: mode=auto, default_loop_scope=session
collected 67 items                                                                                                                                                 

tests/unit/synapseclient/models/synchronous/unit_test_submission.py .............................                                                            [ 43%]
tests/unit/synapseclient/models/synchronous/unit_test_submission_bundle.py ...............                                                                   [ 65%]
tests/unit/synapseclient/models/synchronous/unit_test_submission_status.py .......................                                                           [100%]

@jaymedina jaymedina force-pushed the synpy-1590-submission-model-main branch from 8b50d06 to a061fb4 Compare November 5, 2025 14:46
@jaymedina jaymedina force-pushed the synpy-1590-submission-model-functionality branch from 62ce5fb to f0d7ecc Compare November 5, 2025 15:04
@jaymedina jaymedina marked this pull request as ready for review November 21, 2025 19:29
@jaymedina jaymedina requested a review from a team as a code owner November 21, 2025 19:29
@jaymedina jaymedina force-pushed the synpy-1590-submission-model-main branch from a061fb4 to c748132 Compare December 1, 2025 16:33
@jaymedina jaymedina force-pushed the synpy-1590-submission-model-functionality branch from a9977b2 to b8c0ee5 Compare December 1, 2025 16:35
etag: str,
annotations: typing.Dict[str, typing.Any],
logger: typing.Optional[typing.Any] = None,
) -> typing.Dict[str, typing.Any]:
Copy link
Member

@thomasyu888 thomasyu888 Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussion: Should SubmissionAnnotations be a class structure or is that not needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

etag: The current eTag of the Entity being submitted.
synapse_client: If not passed in and caching was not disabled by `Synapse.allow_client_caching(False)` this will use the last created
instance from the Synapse class constructor.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a description of the return object

The caller must be granted the ACCESS_TYPE.READ on the specified Evaluation.
Furthermore, the caller must be granted the ACCESS_TYPE.READ_PRIVATE_SUBMISSION
to see all data marked as "private" in the SubmissionStatus.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if the caller has ACCESS_TYPE.READ, but not ACCESS_TYPE.READ_PRIVATE_SUBMISSION permissions?

return entity_info
except Exception as e:
raise ValueError(
f"Unable to fetch entity information for {self.entity_id}: {e}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to do
raise ValueError(msg) from e ?

@@ -0,0 +1,920 @@
from dataclasses import dataclass, field
from typing import AsyncGenerator, Dict, Generator, List, Optional, Protocol, Union
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to import Dict or List types anymore. Instead, use the lower case version type hints dict and list.

Returns:
Dictionary containing entity information from the REST API.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a user-facing method, should the dictionary returned be described?

docker_tag_response = await client.rest_get_async(
f"/entity/{self.entity_id}/dockerTag"
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be an api function?

request_body["dockerRepositoryName"] = self.docker_repository_name
if self.docker_digest is not None:
request_body["dockerDigest"] = self.docker_digest

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a list of optional fields and then use a for loop to iterate over them, and them to the request body if they are not none?

"entityId": self.entity_id,
"evaluationId": self.evaluation_id,
"versionNumber": self.version_number,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should self.contributors be here as well? It doesn't appear to be optional.

Raises:
ValueError: If any required attributes are missing.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be an example for this method?

```
"""

if self.entity_id:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would structure this as:

if not self.entity_id:
    raiseValueError(msg)

<code that is in current if block>

This reduces the amount of indented code.


if not self.entity_etag:
raise ValueError("Unable to fetch etag for entity")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this up to right after line 582 where you get the entity tag? That way it would fail faster, and when there wasn't an entity tag, you wouldn't have to the docker checking.

Returns:
A dictionary containing the request body for updating a submission status.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example?

request_body["canCancel"] = self.can_cancel
if self.cancel_requested is not None:
request_body["cancelRequested"] = self.cancel_requested

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion as Submission about turning this into a for loop.

submission_status._set_last_persistent_instance()
submission_statuses.append(submission_status)

return submission_statuses
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a generator method?



def to_submission_annotations(
id: typing.Union[str, int],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: wouldn't it be less verbose to import the classes from typing that we'll need at the beginning of the file instead of accessing them through the library each time?

id: typing.Union[str, int],
etag: str,
annotations: typing.Dict[str, typing.Any],
logger: typing.Optional[typing.Any] = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this really be of type Any instead of a union between logger.Logger and the synapse client logger?

Submission objects as they are retrieved from the API.
Example: Getting submissions for an evaluation
&nbsp;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is &nbsp?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds non-breaking space in the documentation. We previously had issues where the docs didn’t render correctly in the UI after deployment, and this change fixes that.

Comment on lines +489 to +492
except Exception as e:
raise ValueError(
f"Unable to fetch entity information for {self.entity_id}: {e}"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be catching every possible exception here and assuming they're all the same cause, or would it be useful to break the catching up into synapse errors and other possible kinds? Also would a LookupError make more sense here than a ValueError?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could do raise ValueError(msg) from e here. Then the user would still get a ValueError but would also see the exception that was caught as well.

Comment on lines +127 to +144
# async def test_store_submission_with_docker_repository_async(
# self, test_evaluation: Evaluation
# ):
# # GIVEN we would need a Docker repository entity (mocked for this test)
# # This test demonstrates the expected behavior for Docker repository submissions

# # WHEN I create a submission for a Docker repository entity using async method
# # TODO: This would require a real Docker repository entity in a full integration test
# submission = Submission(
# entity_id="syn123456789", # Would be a Docker repository ID
# evaluation_id=test_evaluation.id,
# name=f"Docker Submission {uuid.uuid4()}",
# )

# # THEN the submission should handle Docker-specific attributes
# # (This test would need to be expanded with actual Docker repository setup)
# assert submission.entity_id == "syn123456789"
# assert submission.evaluation_id == test_evaluation.id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be removed?

):
"""Test that storing a submission status without changes shows warning async."""
# GIVEN a submission status that hasn't been modified
# (it already has _last_persistent_instance set from get())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to assert that _last_persistent_instance is some specific value or at least exists since we're depending on that happening outside this specific test?


# THEN the batch update should succeed
assert response is not None
assert "batchToken" in response or response == {} # Response format may vary
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what causes the response formats to vary? should those be different parametrizations for this test?

# This test demonstrates the expected behavior for Docker repository submissions

# WHEN I create a submission for a Docker repository entity
# TODO: This would require a real Docker repository entity in a full integration test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has a JIRA ticket been created for this TODO? if so can we link to it?

Comment on lines +481 to +498
# TODO: Add with SubmissionStatus model tests
# async def test_cancel_submission_successfully(
# self, test_evaluation: Evaluation, test_file: File
# ):
# # GIVEN a submission
# submission = Submission(
# entity_id=test_file.id,
# evaluation_id=test_evaluation.id,
# name=f"Test Submission for Cancellation {uuid.uuid4()}",
# )
# created_submission = submission.store(synapse_client=self.syn)
# self.schedule_for_cleanup(created_submission.id)

# # WHEN I cancel the submission
# cancelled_submission = created_submission.cancel(synapse_client=self.syn)

# # THEN the submission should be cancelled
# assert cancelled_submission.id == created_submission.id
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to remove this commented out section and create a JIRA issue so that we can track and prioritize?

Comment on lines +102 to +104
assert (
submission_status.status is not None
) # Should have some status (e.g., "RECEIVED")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment as similar one above

client.logger.warning(
f"Annotation '{key}' has an empty value list and will be skipped"
)
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this continue redundant, since it's at the bottom of the loop?

@@ -1,199 +1,674 @@
def test_create_submission_async():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file testing the non-OOP code? I'm assuming that code is deprecated and hasn't been removed yet. If so, should these tests remain until it is sunsetted?

@andrewelamb
Copy link
Contributor

Looking good so far! I didn't see any deprecations, will you be doing that as part of this PR?

@jaymedina jaymedina force-pushed the synpy-1590-submission-model-main branch from c748132 to 31d9583 Compare December 2, 2025 19:47
# evaluation_id is required when annotations are provided for scopeId
if self.evaluation_id is None:
raise ValueError(
"Your submission status object is missing the 'evaluation_id' attribute. This attribute is required when submissions are updated with annotations. Please retrieve your submission status with .get() to populate this field."
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewelamb thanks for bringing up this question because it exposed a flaw in the current logic for SubmissionStatus.

A while ago on Slack, we had this conversation on the differences between the annotations object and the new submission_annotations object.

Long story short:

  1. annotations is the legacy object used when Submission Views didn't exist yet, and CnB was using the leaderboard wiki
  2. submission_annotations is the new annotations object, and these annotation types show up in submission views

I agreed with @BryanFauble in wanting to support both annotation types because annotations has not yet been sunsetted.

The catch is this annotation type requires an evaluation_id attribute to be populated, which complicates things a bit because this is not an attribute that's part of the SubmissionStatus response object (🤷‍♀️). But it is part of the Submission response object, so I did a little hacky thing and called the get_submission function which exposes the Evaluation service that retrieves a Submission response object, grabbed the evaluation ID from there, and assigned it as an attribute of the Submission_Status_ object.

This all works fine if the user has READ_PRIVATE_SUBMISSION access and beyond, but not if they only have READ access. If they don't have READ_PRIVATE_SUBMISSION access, they can't grab any submission (not even their own - see documentation), and the call for the submission status will lead to a "not enough permissions" failure, even though they technically have the right permissions to get the status but not the submission.

The user should safely assume they don't need more than READ access to grab the submission status, so I'll have to remove this logic.

Possible ways forward:

  1. Keep supporting the legacy annotations, but require the user to manually populate the evaluation_id attribute when necessary (i.e. when storing their SubmissionStatus with a populated annotations attribute). This is pretty straightforward to implement, except it's confusing from a user-perspective because it now involves supporting an attribute (evaluation_id) that you need in some situations and not others.
  2. Get rid of support for legacy annotations, and the evaluation_id attribute.

I would love some input on this. Right now I lean towards 2.


For @vpchung:

The tldr is that supporting the legacy annotations object has turned out to be more of a headache than anticipated, because of the scopeId (AKA evaluation ID) attribute and the different permission requirements between getting a Submission and getting a SubmissionStatus from Synapse. Which of the 2 possible ways forward listed above do you prefer?


side note: I have some lingering questions for the platform team on why some things were designed the way they are, which I've left on Slack here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would keep vote to keep supporting the annotations object until v5.0. But:

  • If it hasn't yet, deprecate it with a warning to start using the new object ASAP.
  • Raise an exception when the user only has READ_PRIVATE_SUBMISSION permissions that:
  1. That they don't have the correct permissions
  2. That they should start using the new object ASAP
  3. As a work-around they can supply the evaluation_id (is that correct?)

* always log warning

* check assert called with

* patch object on class instance

* Revert "always log warning"

This reverts commit ae63c81.

* update patch target

* re-add erroneously removed line
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants