-
Notifications
You must be signed in to change notification settings - Fork 68
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
[SYNPY-1322] Object Orientated Programming Interfaces #1013
Changes from 12 commits
625b0ba
6105a71
cebe138
89cd7c7
40250fb
dd0eeb0
03cf453
dcc29f0
77a8d6b
b27cf12
244c69f
55ee852
65a1c70
57da4e1
5bdf4c4
1a11b17
f988b5c
5d5f9f5
6b5b9af
86a474e
dfbddcc
6b74371
84a5369
d370b46
3aec4ff
93bacca
bbe905f
410de22
be9f193
cc049b0
53e4c22
8fef247
a19d77a
377df74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# These are all of the models that are used by the Synapse client. | ||
from .annotations import set_annotations | ||
|
||
|
||
__all__ = [ | ||
"set_annotations", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
""" | ||
The purpose of this module is to provide any functions that are needed to interact with | ||
annotations that are not cleanly provided by the synapseclient library. | ||
""" | ||
import json | ||
|
||
from dataclasses import asdict | ||
|
||
from typing import TYPE_CHECKING, Optional | ||
from synapseclient import Synapse | ||
|
||
if TYPE_CHECKING: | ||
from synapseclient.models import Annotations | ||
|
||
|
||
def set_annotations( | ||
annotations: "Annotations", synapse_client: Optional[Synapse] = None | ||
): | ||
"""Call to synapse and set the annotations for the given input. | ||
|
||
:param annotations: The annotations to set. This is expected to have the id, etag, and annotations filled in. | ||
:return: _description_ | ||
""" | ||
annotations_dict = asdict(annotations) | ||
|
||
# TODO: Is there a more elegant way to handle this - This is essentially being used | ||
# TODO: to remove any fields that are not expected by the REST API. | ||
filtered_dict = {k: v for k, v in annotations_dict.items() if k != "is_loaded"} | ||
|
||
# TODO: This `restPUT` returns back a dict (or string) - Could we use: | ||
# TODO: https://github.com/konradhalas/dacite to convert the dict to an object? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do like the look of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure yet - I'll have to give it some more thought. One of the things we will need to do it at least have a thin translation layer between the REST api and the dataclasses because the names we are giving them in the python client are snake_case, vs the REST api is all in camelCase. |
||
return ( | ||
Synapse() | ||
.get_client(synapse_client=synapse_client) | ||
.restPUT( | ||
f"/entity/{annotations.id}/annotations2", | ||
body=json.dumps(filtered_dict), | ||
) | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# These are all of the models that are used by the Synapse client. | ||
from synapseclient.models.annotations import ( | ||
Annotations, | ||
AnnotationsValue, | ||
AnnotationsValueType, | ||
) | ||
from synapseclient.models.file import File | ||
from synapseclient.models.folder import Folder | ||
from synapseclient.models.project import Project | ||
|
||
__all__ = [ | ||
"File", | ||
"Folder", | ||
"Project", | ||
"Annotations", | ||
"AnnotationsValue", | ||
"AnnotationsValueType", | ||
] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# TODO |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import asyncio | ||
|
||
from enum import Enum | ||
from dataclasses import dataclass | ||
from typing import Dict, List, Optional, Union | ||
from synapseclient.api import set_annotations | ||
from opentelemetry import trace | ||
|
||
from synapseclient import Synapse | ||
|
||
|
||
tracer = trace.get_tracer("synapseclient") | ||
|
||
|
||
class AnnotationsValueType(str, Enum): | ||
"""The acceptable types that an annotation value can be.""" | ||
|
||
STRING = "STRING" | ||
DOUBLE = "DOUBLE" | ||
LONG = "LONG" | ||
TIMESTAMP_MS = "TIMESTAMP_MS" | ||
BOOLEAN = "BOOLEAN" | ||
|
||
|
||
@dataclass() | ||
class AnnotationsValue: | ||
"""A specific type of annotation and the values that are of that type.""" | ||
|
||
type: AnnotationsValueType | ||
# TODO: What are all the python types we are going to accept here | ||
value: List[Union[str, bool]] | ||
|
||
|
||
@dataclass() | ||
class Annotations: | ||
BryanFauble marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Annotations that can be applied to a number of Synapse resources to provide additional information.""" | ||
|
||
annotations: Dict[str, AnnotationsValue] | ||
""" Additional metadata associated with the object. The key is the name of your | ||
desired annotations. The value is an object containing a list of string values | ||
(use empty list to represent no values for key) and the value type associated with | ||
all values in the list | ||
""" | ||
|
||
id: Optional[str] = None | ||
"""ID of the object to which this annotation belongs. Not required if being used as | ||
a member variable on another class.""" | ||
|
||
etag: Optional[str] = None | ||
""" Etag of the object to which this annotation belongs. To update an AnnotationV2, | ||
this field must match the current etag on the object. Not required if being used as | ||
a member variable on another class.""" | ||
|
||
is_loaded: bool = False | ||
|
||
async def store( | ||
self, | ||
synapse_client: Optional[Synapse] = None, | ||
): | ||
"""Storing annotations to synapse.""" | ||
# TODO: Validation that id and etag are present | ||
|
||
print(f"Storing annotations for id: {self.id}, etag: {self.etag}") | ||
with tracer.start_as_current_span(f"Annotation_store: {self.id}"): | ||
loop = asyncio.get_event_loop() | ||
await loop.run_in_executor( | ||
None, | ||
lambda: set_annotations( | ||
annotations=self, synapse_client=synapse_client | ||
), | ||
) | ||
print(f"annotations store for {self.id} complete") | ||
# TODO: From the returned call do we need to update anything in the root object? | ||
return self | ||
|
||
async def get(self): | ||
"""Get the annotations from synapse.""" | ||
print(f"Getting annotations for id: {self.id}, etag: {self.etag}") | ||
await asyncio.sleep(1) | ||
self.is_loaded = True |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# TODO |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import asyncio | ||
from dataclasses import dataclass | ||
from typing import Dict, Union | ||
from opentelemetry import trace, context | ||
from synapseclient.models import AnnotationsValue, Annotations | ||
|
||
# import uuid | ||
BryanFauble marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
from synapseclient.entity import File as SynapseFile | ||
from synapseclient import Synapse | ||
|
||
from typing import Optional, TYPE_CHECKING | ||
|
||
|
||
if TYPE_CHECKING: | ||
from synapseclient.models import Folder, Project | ||
|
||
|
||
tracer = trace.get_tracer("synapseclient") | ||
|
||
|
||
@dataclass() | ||
class File: | ||
BryanFauble marked this conversation as resolved.
Show resolved
Hide resolved
|
||
id: str | ||
"""The unique immutable ID for this file. A new ID will be generated for new Files. | ||
Once issued, this ID is guaranteed to never change or be re-issued""" | ||
|
||
name: str | ||
"""The name of this entity. Must be 256 characters or less. | ||
Names may only contain: letters, numbers, spaces, underscores, hyphens, periods, | ||
plus signs, apostrophes, and parentheses""" | ||
|
||
path: str | ||
# TODO - Should a file also have a folder, or a method that figures out the folder class? | ||
|
||
description: Optional[str] = None | ||
"""The description of this file. Must be 1000 characters or less.""" | ||
|
||
etag: Optional[str] = None | ||
created_on: Optional[str] = None | ||
modified_on: Optional[str] = None | ||
created_by: Optional[str] = None | ||
modified_by: Optional[str] = None | ||
parent_id: Optional[str] = None | ||
concrete_type: Optional[str] = None | ||
version_number: Optional[int] = None | ||
version_label: Optional[str] = None | ||
version_comment: Optional[str] = None | ||
is_latest_version: Optional[bool] = False | ||
data_file_handle_id: Optional[str] = None | ||
file_name_override: Optional[str] = None | ||
|
||
annotations: Optional[Dict[str, AnnotationsValue]] = None | ||
"""Additional metadata associated with the folder. The key is the name of your | ||
desired annotations. The value is an object containing a list of values | ||
(use empty list to represent no values for key) and the value type associated with | ||
all values in the list.""" | ||
|
||
is_loaded: bool = False | ||
|
||
# TODO: How the parent is stored/referenced needs to be thought through | ||
async def store( | ||
self, | ||
parent: Union["Folder", "Project"], | ||
synapse_client: Optional[Synapse] = None, | ||
): | ||
"""Storing file to synapse.""" | ||
with tracer.start_as_current_span(f"File_Store: {self.path}"): | ||
# TODO - We need to add in some validation before the store to verify we have enough | ||
# information to store the data | ||
|
||
# Call synapse | ||
loop = asyncio.get_event_loop() | ||
synapse_file = SynapseFile(path=self.path, name=self.name, parent=parent.id) | ||
# TODO: Propogating OTEL context is not working in this case | ||
entity = await loop.run_in_executor( | ||
None, | ||
lambda: Synapse() | ||
.get_client(synapse_client=synapse_client) | ||
.store(obj=synapse_file, opentelemetry_context=context.get_current()), | ||
) | ||
print(entity) | ||
self.id = entity.id | ||
self.etag = entity.etag | ||
|
||
print(f"Stored file {self.name}, id: {self.id}: {self.path}") | ||
|
||
if self.annotations: | ||
result = await Annotations( | ||
id=self.id, etag=self.etag, annotations=self.annotations | ||
).store(synapse_client=synapse_client) | ||
print(result) | ||
|
||
return self | ||
|
||
async def get(self): | ||
"""Get metadata about the folder from synapse.""" | ||
print(f"Getting file {self.name}") | ||
await asyncio.sleep(1) | ||
self.is_loaded = True |
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.
There is another dataclass library that adds some more functionality. Its
asdict
function has anexclude
argument that you can pass to leave out class attributes you don't want in your dictionary:You'd still be hard-coding the excluded values though. Not sure of any other ways aside from implementing an
asdict
method specific to the class that excludes attributes not used in the API.Edit: if you used a base class you could potentially implement an
asdict
function that could be reused across all extending classesThere 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 idea - but i'm starting to think in the other direction now. What I mean by this is specify only those things I want to include and have a dataclass -> json/dict mapping process to format the input for the REST API as the API is expecting.