From 08ac76cf09fca0b99178d196dc61dee81a99cc6e Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Thu, 1 Jun 2023 23:48:04 +0100 Subject: [PATCH 01/12] untracked API to Core --- src/specklepy/api/client.py | 10 +- src/specklepy/api/credentials.py | 115 +-- src/specklepy/api/operations.py | 73 +- src/specklepy/api/resource.py | 2 +- src/specklepy/api/resources/active_user.py | 194 +---- src/specklepy/api/resources/branch.py | 144 +--- src/specklepy/api/resources/commit.py | 136 +--- src/specklepy/api/resources/other_user.py | 126 +-- src/specklepy/api/resources/server.py | 126 +-- src/specklepy/api/resources/stream.py | 520 +----------- src/specklepy/api/wrapper.py | 4 +- src/specklepy/core/api/__init__.py | 0 src/specklepy/core/api/credentials.py | 152 ++++ src/specklepy/core/api/operations.py | 139 ++++ src/specklepy/core/api/resources/__init__.py | 0 .../core/api/resources/active_user.py | 264 ++++++ src/specklepy/core/api/resources/branch.py | 219 +++++ src/specklepy/core/api/resources/commit.py | 237 ++++++ .../core/api/resources/other_user.py | 172 ++++ src/specklepy/core/api/resources/server.py | 174 ++++ src/specklepy/core/api/resources/stream.py | 751 ++++++++++++++++++ src/specklepy/transports/server/server.py | 2 +- 22 files changed, 2208 insertions(+), 1352 deletions(-) create mode 100644 src/specklepy/core/api/__init__.py create mode 100644 src/specklepy/core/api/credentials.py create mode 100644 src/specklepy/core/api/operations.py create mode 100644 src/specklepy/core/api/resources/__init__.py create mode 100644 src/specklepy/core/api/resources/active_user.py create mode 100644 src/specklepy/core/api/resources/branch.py create mode 100644 src/specklepy/core/api/resources/commit.py create mode 100644 src/specklepy/core/api/resources/other_user.py create mode 100644 src/specklepy/core/api/resources/server.py create mode 100644 src/specklepy/core/api/resources/stream.py diff --git a/src/specklepy/api/client.py b/src/specklepy/api/client.py index f64d657e..dd7967d0 100644 --- a/src/specklepy/api/client.py +++ b/src/specklepy/api/client.py @@ -9,7 +9,7 @@ from gql.transport.websockets import WebsocketsTransport from specklepy.api import resources -from specklepy.api.credentials import Account, get_account_from_token +from specklepy.core.api.credentials import Account, get_account_from_token from specklepy.api.resources import ( active_user, branch, @@ -60,7 +60,7 @@ class SpeckleClient: USE_SSL = True def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None: - metrics.track(metrics.CLIENT, custom_props={"name": "create"}) + #metrics.track(metrics.CLIENT, custom_props={"name": "create"}) ws_protocol = "ws" http_protocol = "http" @@ -129,7 +129,7 @@ def authenticate_with_token(self, token: str) -> None: token {str} -- an api token """ self.account = get_account_from_token(token, self.url) - metrics.track(metrics.CLIENT, self.account, {"name": "authenticate with token"}) + #metrics.track(metrics.CLIENT, self.account, {"name": "authenticate with token"}) self._set_up_client() def authenticate_with_account(self, account: Account) -> None: @@ -141,12 +141,12 @@ def authenticate_with_account(self, account: Account) -> None: account {Account} -- the account object which can be found with `get_default_account` or `get_local_accounts` """ - metrics.track(metrics.CLIENT, account, {"name": "authenticate with account"}) + #metrics.track(metrics.CLIENT, account, {"name": "authenticate with account"}) self.account = account self._set_up_client() def _set_up_client(self) -> None: - metrics.track(metrics.CLIENT, self.account, {"name": "set up client"}) + #metrics.track(metrics.CLIENT, self.account, {"name": "set up client"}) headers = { "Authorization": f"Bearer {self.account.token}", "Content-Type": "application/json", diff --git a/src/specklepy/api/credentials.py b/src/specklepy/api/credentials.py index 9d504411..4c3454c7 100644 --- a/src/specklepy/api/credentials.py +++ b/src/specklepy/api/credentials.py @@ -9,37 +9,11 @@ from specklepy.logging.exceptions import SpeckleException from specklepy.transports.sqlite import SQLiteTransport - -class UserInfo(BaseModel): - name: Optional[str] = None - email: Optional[str] = None - company: Optional[str] = None - id: Optional[str] = None - - -class Account(BaseModel): - isDefault: bool = False - token: Optional[str] = None - refreshToken: Optional[str] = None - serverInfo: ServerInfo = Field(default_factory=ServerInfo) - userInfo: UserInfo = Field(default_factory=UserInfo) - id: Optional[str] = None - - def __repr__(self) -> str: - return ( - f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}," - f" isDefault: {self.isDefault})" - ) - - def __str__(self) -> str: - return self.__repr__() - - @classmethod - def from_token(cls, token: str, server_url: str = None): - acct = cls(token=token) - acct.serverInfo.url = server_url - return acct - +# following imports seem to be unnecessary, but they need to stay +# to not break the scripts using these functions as non-core +from specklepy.core.api.credentials import (UserInfo, Account, StreamWrapper, + get_account_from_token, + get_local_accounts as core_get_local_accounts) def get_local_accounts(base_path: Optional[str] = None) -> List[Account]: """Gets all the accounts present in this environment @@ -51,41 +25,7 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]: List[Account] -- list of all local accounts or an empty list if no accounts were found """ - accounts: List[Account] = [] - try: - account_storage = SQLiteTransport(scope="Accounts", base_path=base_path) - res = account_storage.get_all_objects() - account_storage.close() - if res: - accounts.extend(Account.parse_raw(r[1]) for r in res) - except SpeckleException: - # cannot open SQLiteTransport, probably because of the lack - # of disk write permissions - pass - - json_acct_files = [] - json_path = str(speckle_path_provider.accounts_folder_path()) - try: - os.makedirs(json_path, exist_ok=True) - json_acct_files.extend( - file for file in os.listdir(json_path) if file.endswith(".json") - ) - - except Exception: - # cannot find or get the json account paths - pass - - if json_acct_files: - try: - accounts.extend( - Account.parse_file(os.path.join(json_path, json_file)) - for json_file in json_acct_files - ) - except Exception as ex: - raise SpeckleException( - "Invalid json accounts could not be read. Please fix or remove them.", - ex, - ) from ex + accounts = core_get_local_accounts(base_path) metrics.track( metrics.ACCOUNTS, @@ -97,7 +37,6 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]: return accounts - def get_default_account(base_path: Optional[str] = None) -> Optional[Account]: """ Gets this environment's default account if any. If there is no default, @@ -108,7 +47,7 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]: Returns: Account -- the default account or None if no local accounts were found """ - accounts = get_local_accounts(base_path=base_path) + accounts = core_get_local_accounts(base_path=base_path) if not accounts: return None @@ -119,42 +58,4 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]: metrics.initialise_tracker(default) return default - - -def get_account_from_token(token: str, server_url: str = None) -> Account: - """Gets the local account for the token if it exists - Arguments: - token {str} -- the api token - - Returns: - Account -- the local account with this token or a shell account containing - just the token and url if no local account is found - """ - accounts = get_local_accounts() - if not accounts: - return Account.from_token(token, server_url) - - acct = next((acc for acc in accounts if acc.token == token), None) - if acct: - return acct - - if server_url: - url = server_url.lower() - acct = next( - (acc for acc in accounts if url in acc.serverInfo.url.lower()), None - ) - if acct: - return acct - - return Account.from_token(token, server_url) - - -class StreamWrapper: - def __init__(self, url: str = None) -> None: - raise SpeckleException( - message=( - "The StreamWrapper has moved as of v2.6.0! Please import from" - " specklepy.api.wrapper" - ), - exception=DeprecationWarning(), - ) + \ No newline at end of file diff --git a/src/specklepy/api/operations.py b/src/specklepy/api/operations.py index 600f0641..016c0281 100644 --- a/src/specklepy/api/operations.py +++ b/src/specklepy/api/operations.py @@ -7,6 +7,11 @@ from specklepy.transports.abstract_transport import AbstractTransport from specklepy.transports.sqlite import SQLiteTransport +from specklepy.core.api.operations import (send as core_send, + receive as core_receive, + serialize as core_serialize, + deserialize as core_deserialize) + def send( base: Base, @@ -24,31 +29,13 @@ def send( Returns: str -- the object id of the sent object """ - - if not transports and not use_default_cache: - raise SpeckleException( - message=( - "You need to provide at least one transport: cannot send with an empty" - " transport list and no default cache" - ) - ) - - if isinstance(transports, AbstractTransport): - transports = [transports] + obj_hash = core_send(base, transports, use_default_cache) if transports is None: metrics.track(metrics.SEND) - transports = [] else: metrics.track(metrics.SEND, getattr(transports[0], "account", None)) - if use_default_cache: - transports.insert(0, SQLiteTransport()) - - serializer = BaseObjectSerializer(write_transports=transports) - - obj_hash, _ = serializer.write_json(base=base) - return obj_hash @@ -56,15 +43,6 @@ def receive( obj_id: str, remote_transport: Optional[AbstractTransport] = None, local_transport: Optional[AbstractTransport] = None, -) -> Base: - metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None)) - return _untracked_receive(obj_id, remote_transport, local_transport) - - -def _untracked_receive( - obj_id: str, - remote_transport: Optional[AbstractTransport] = None, - local_transport: Optional[AbstractTransport] = None, ) -> Base: """Receives an object from a transport. @@ -77,29 +55,16 @@ def _untracked_receive( Returns: Base -- the base object """ - if not local_transport: - local_transport = SQLiteTransport() - - serializer = BaseObjectSerializer(read_transport=local_transport) - - # try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport - obj_string = local_transport.get_object(obj_id) - if obj_string: - return serializer.read_json(obj_string=obj_string) - - if not remote_transport: - raise SpeckleException( - message=( - "Could not find the specified object using the local transport, and you" - " didn't provide a fallback remote from which to pull it." - ) - ) + metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None)) + return core_receive(obj_id, remote_transport, local_transport) - obj_string = remote_transport.copy_object_and_children( - id=obj_id, target_transport=local_transport - ) - return serializer.read_json(obj_string=obj_string) +def _untracked_receive( + obj_id: str, + remote_transport: Optional[AbstractTransport] = None, + local_transport: Optional[AbstractTransport] = None, +) -> Base: + return receive(obj_id, remote_transport, local_transport) def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str: @@ -117,10 +82,8 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str str -- the serialized object """ metrics.track(metrics.SERIALIZE) - serializer = BaseObjectSerializer(write_transports=write_transports) - - return serializer.write_json(base)[1] + return core_serialize(base, write_transports) def deserialize( obj_string: str, read_transport: Optional[AbstractTransport] = None @@ -142,12 +105,8 @@ def deserialize( Base -- the deserialized object """ metrics.track(metrics.DESERIALIZE) - if not read_transport: - read_transport = SQLiteTransport() - - serializer = BaseObjectSerializer(read_transport=read_transport) - return serializer.read_json(obj_string=obj_string) + return core_deserialize(obj_string, read_transport) __all__ = ["receive", "send", "serialize", "deserialize"] diff --git a/src/specklepy/api/resource.py b/src/specklepy/api/resource.py index 47e24dfa..a6289569 100644 --- a/src/specklepy/api/resource.py +++ b/src/specklepy/api/resource.py @@ -4,7 +4,7 @@ from gql.transport.exceptions import TransportQueryError from graphql import DocumentNode -from specklepy.api.credentials import Account +from specklepy.core.api.credentials import Account from specklepy.logging.exceptions import ( GraphQLException, SpeckleException, diff --git a/src/specklepy/api/resources/active_user.py b/src/specklepy/api/resources/active_user.py index 9f9128a6..cbc0ca6d 100644 --- a/src/specklepy/api/resources/active_user.py +++ b/src/specklepy/api/resources/active_user.py @@ -8,10 +8,10 @@ from specklepy.logging import metrics from specklepy.logging.exceptions import SpeckleException -NAME = "active_user" +from specklepy.core.api.resources.active_user import NAME, Resource as Core_Resource -class Resource(ResourceBase): +class Resource(Core_Resource): """API Access class for users""" def __init__(self, account, basepath, client, server_version) -> None: @@ -36,27 +36,8 @@ def get(self) -> User: User -- the retrieved user """ metrics.track(metrics.USER, self.account, {"name": "get"}) - query = gql( - """ - query User { - activeUser { - id - email - name - bio - company - avatar - verified - profiles - role - } - } - """ - ) - - params = {} - - return self.make_request(query=query, params=params, return_type="activeUser") + + return super().get() def update( self, @@ -77,105 +58,8 @@ def update( bool -- True if your profile was updated successfully """ metrics.track(metrics.USER, self.account, {"name": "update"}) - query = gql( - """ - mutation UserUpdate($user: UserUpdateInput!) { - userUpdate(user: $user) - } - """ - ) - params = {"name": name, "company": company, "bio": bio, "avatar": avatar} - - params = {"user": {k: v for k, v in params.items() if v is not None}} - - if not params["user"]: - return SpeckleException( - message=( - "You must provide at least one field to update your user profile" - ) - ) - - return self.make_request( - query=query, params=params, return_type="userUpdate", parse_response=False - ) - - def activity( - self, - limit: int = 20, - action_type: Optional[str] = None, - before: Optional[datetime] = None, - after: Optional[datetime] = None, - cursor: Optional[datetime] = None, - ): - """ - Get the activity from a given stream in an Activity collection. - Step into the activity `items` for the list of activity. - If no id argument is provided, will return the current authenticated user's - activity (as extracted from the authorization header). - - Note: all timestamps arguments should be `datetime` of any tz as they will be - converted to UTC ISO format strings - - user_id {str} -- the id of the user to get the activity from - action_type {str} -- filter results to a single action type - (eg: `commit_create` or `commit_receive`) - limit {int} -- max number of Activity items to return - before {datetime} -- latest cutoff for activity - (ie: return all activity _before_ this time) - after {datetime} -- oldest cutoff for activity - (ie: return all activity _after_ this time) - cursor {datetime} -- timestamp cursor for pagination - """ - - query = gql( - """ - query UserActivity( - $action_type: String, - $before:DateTime, - $after: DateTime, - $cursor: DateTime, - $limit: Int - ){ - activeUser { - activity( - actionType: $action_type, - before: $before, - after: $after, - cursor: $cursor, - limit: $limit - ) { - totalCount - cursor - items { - actionType - info - userId - streamId - resourceId - resourceType - message - time - } - } - } - } - """ - ) - - params = { - "limit": limit, - "action_type": action_type, - "before": before.astimezone(timezone.utc).isoformat() if before else before, - "after": after.astimezone(timezone.utc).isoformat() if after else after, - "cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor, - } - - return self.make_request( - query=query, - params=params, - return_type=["activeUser", "activity"], - schema=ActivityCollection, - ) + + return super().update(name, company, bio, avatar) def get_all_pending_invites(self) -> List[PendingStreamCollaborator]: """Get all of the active user's pending stream invites @@ -187,35 +71,8 @@ def get_all_pending_invites(self) -> List[PendingStreamCollaborator]: -- a list of pending invites for the current user """ metrics.track(metrics.INVITE, self.account, {"name": "get"}) - self._check_invites_supported() - - query = gql( - """ - query StreamInvites { - streamInvites{ - id - token - inviteId - streamId - streamName - title - role - invitedBy { - id - name - company - avatar - } - } - } - """ - ) - - return self.make_request( - query=query, - return_type="streamInvites", - schema=PendingStreamCollaborator, - ) + + return super().get_all_pending_invites() def get_pending_invite( self, stream_id: str, token: Optional[str] = None @@ -234,36 +91,5 @@ def get_pending_invite( -- the invite for the given stream (or None if it isn't found) """ metrics.track(metrics.INVITE, self.account, {"name": "get"}) - self._check_invites_supported() - - query = gql( - """ - query StreamInvite($streamId: String!, $token: String) { - streamInvite(streamId: $streamId, token: $token) { - id - token - streamId - streamName - title - role - invitedBy { - id - name - company - avatar - } - } - } - """ - ) - - params = {"streamId": stream_id} - if token: - params["token"] = token - - return self.make_request( - query=query, - params=params, - return_type="streamInvite", - schema=PendingStreamCollaborator, - ) + + return super().get_pending_invite(stream_id, token) diff --git a/src/specklepy/api/resources/branch.py b/src/specklepy/api/resources/branch.py index 100423dd..de8b268a 100644 --- a/src/specklepy/api/resources/branch.py +++ b/src/specklepy/api/resources/branch.py @@ -6,10 +6,10 @@ from specklepy.api.resource import ResourceBase from specklepy.logging import metrics -NAME = "branch" +from specklepy.core.api.resources.branch import NAME, Resource as Core_Resource -class Resource(ResourceBase): +class Resource(Core_Resource): """API Access class for branches""" def __init__(self, account, basepath, client) -> None: @@ -34,24 +34,8 @@ def create( id {str} -- the newly created branch's id """ metrics.track(metrics.BRANCH, self.account, {"name": "create"}) - query = gql( - """ - mutation BranchCreate($branch: BranchCreateInput!) { - branchCreate(branch: $branch) - } - """ - ) - params = { - "branch": { - "streamId": stream_id, - "name": name, - "description": description, - } - } - - return self.make_request( - query=query, params=params, return_type="branchCreate", parse_response=False - ) + + return super().create(stream_id, name, description) def get(self, stream_id: str, name: str, commits_limit: int = 10): """Get a branch by name from a stream @@ -65,41 +49,8 @@ def get(self, stream_id: str, name: str, commits_limit: int = 10): Branch -- the fetched branch with its latest commits """ metrics.track(metrics.BRANCH, self.account, {"name": "get"}) - query = gql( - """ - query BranchGet($stream_id: String!, $name: String!, $commits_limit: Int!) { - stream(id: $stream_id) { - branch(name: $name) { - id, - name, - description, - commits (limit: $commits_limit) { - totalCount, - cursor, - items { - id, - referencedObject, - sourceApplication, - totalChildrenCount, - message, - authorName, - authorId, - branchName, - parents, - createdAt - } - } - } - } - } - """ - ) - - params = {"stream_id": stream_id, "name": name, "commits_limit": commits_limit} - - return self.make_request( - query=query, params=params, return_type=["stream", "branch"] - ) + + return super().get(stream_id, name, commits_limit) def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10): """Get a list of branches from a given stream @@ -113,49 +64,8 @@ def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10 List[Branch] -- the branches on the stream """ metrics.track(metrics.BRANCH, self.account, {"name": "get"}) - query = gql( - """ - query BranchesGet( - $stream_id: String!, - $branches_limit: Int!, - $commits_limit: Int! - ) { - stream(id: $stream_id) { - branches(limit: $branches_limit) { - items { - id - name - description - commits(limit: $commits_limit) { - totalCount - items{ - id - message - referencedObject - sourceApplication - parents - authorId - authorName - branchName - createdAt - } - } - } - } - } - } - """ - ) - - params = { - "stream_id": stream_id, - "branches_limit": branches_limit, - "commits_limit": commits_limit, - } - - return self.make_request( - query=query, params=params, return_type=["stream", "branches", "items"] - ) + + return super().list(stream_id, branches_limit, commits_limit) def update( self, @@ -176,28 +86,8 @@ def update( bool -- True if update is successful """ metrics.track(metrics.BRANCH, self.account, {"name": "update"}) - query = gql( - """ - mutation BranchUpdate($branch: BranchUpdateInput!) { - branchUpdate(branch: $branch) - } - """ - ) - params = { - "branch": { - "streamId": stream_id, - "id": branch_id, - } - } - - if name: - params["branch"]["name"] = name - if description: - params["branch"]["description"] = description - - return self.make_request( - query=query, params=params, return_type="branchUpdate", parse_response=False - ) + + return super().update(stream_id, branch_id, name, description) def delete(self, stream_id: str, branch_id: str): """Delete a branch @@ -210,16 +100,4 @@ def delete(self, stream_id: str, branch_id: str): bool -- True if deletion is successful """ metrics.track(metrics.BRANCH, self.account, {"name": "delete"}) - query = gql( - """ - mutation BranchDelete($branch: BranchDeleteInput!) { - branchDelete(branch: $branch) - } - """ - ) - - params = {"branch": {"streamId": stream_id, "id": branch_id}} - - return self.make_request( - query=query, params=params, return_type="branchDelete", parse_response=False - ) + return super().delete(stream_id, branch_id) diff --git a/src/specklepy/api/resources/commit.py b/src/specklepy/api/resources/commit.py index fa190253..96b358ea 100644 --- a/src/specklepy/api/resources/commit.py +++ b/src/specklepy/api/resources/commit.py @@ -6,10 +6,10 @@ from specklepy.api.resource import ResourceBase from specklepy.logging import metrics -NAME = "commit" +from specklepy.core.api.resources.commit import NAME, Resource as Core_Resource -class Resource(ResourceBase): +class Resource(Core_Resource): """API Access class for commits""" def __init__(self, account, basepath, client) -> None: @@ -32,32 +32,9 @@ def get(self, stream_id: str, commit_id: str) -> Commit: Returns: Commit -- the retrieved commit object """ - query = gql( - """ - query Commit($stream_id: String!, $commit_id: String!) { - stream(id: $stream_id) { - commit(id: $commit_id) { - id - message - referencedObject - authorId - authorName - authorAvatar - branchName - createdAt - sourceApplication - totalChildrenCount - parents - } - } - } - """ - ) - params = {"stream_id": stream_id, "commit_id": commit_id} + metrics.track(metrics.COMMIT, self.account, {"name": "get"}) - return self.make_request( - query=query, params=params, return_type=["stream", "commit"] - ) + return super().get(stream_id, commit_id) def list(self, stream_id: str, limit: int = 10) -> List[Commit]: """ @@ -70,36 +47,9 @@ def list(self, stream_id: str, limit: int = 10) -> List[Commit]: Returns: List[Commit] -- a list of the most recent commit objects """ - metrics.track(metrics.COMMIT, self.account, {"name": "get"}) - query = gql( - """ - query Commits($stream_id: String!, $limit: Int!) { - stream(id: $stream_id) { - commits(limit: $limit) { - items { - id - message - referencedObject - authorName - authorId - authorName - authorAvatar - branchName - createdAt - sourceApplication - totalChildrenCount - parents - } - } - } - } - """ - ) - params = {"stream_id": stream_id, "limit": limit} + metrics.track(metrics.COMMIT, self.account, {"name": "list"}) - return self.make_request( - query=query, params=params, return_type=["stream", "commits", "items"] - ) + return super().list(stream_id, limit) def create( self, @@ -129,27 +79,8 @@ def create( str -- the id of the created commit """ metrics.track(metrics.COMMIT, self.account, {"name": "create"}) - query = gql( - """ - mutation CommitCreate ($commit: CommitCreateInput!) - { commitCreate(commit: $commit)} - """ - ) - params = { - "commit": { - "streamId": stream_id, - "branchName": branch_name, - "objectId": object_id, - "message": message, - "sourceApplication": source_application, - } - } - if parents: - params["commit"]["parents"] = parents - - return self.make_request( - query=query, params=params, return_type="commitCreate", parse_response=False - ) + + return super().create(stream_id, object_id, branch_name, message, source_application, parents) def update(self, stream_id: str, commit_id: str, message: str) -> bool: """ @@ -165,19 +96,8 @@ def update(self, stream_id: str, commit_id: str, message: str) -> bool: bool -- True if the operation succeeded """ metrics.track(metrics.COMMIT, self.account, {"name": "update"}) - query = gql( - """ - mutation CommitUpdate($commit: CommitUpdateInput!) - { commitUpdate(commit: $commit)} - """ - ) - params = { - "commit": {"streamId": stream_id, "id": commit_id, "message": message} - } - return self.make_request( - query=query, params=params, return_type="commitUpdate", parse_response=False - ) + return super().update(stream_id, commit_id, message) def delete(self, stream_id: str, commit_id: str) -> bool: """ @@ -192,17 +112,8 @@ def delete(self, stream_id: str, commit_id: str) -> bool: bool -- True if the operation succeeded """ metrics.track(metrics.COMMIT, self.account, {"name": "delete"}) - query = gql( - """ - mutation CommitDelete($commit: CommitDeleteInput!) - { commitDelete(commit: $commit)} - """ - ) - params = {"commit": {"streamId": stream_id, "id": commit_id}} - return self.make_request( - query=query, params=params, return_type="commitDelete", parse_response=False - ) + return super().delete(stream_id, commit_id) def received( self, @@ -215,29 +126,4 @@ def received( Mark a commit object a received by the source application. """ metrics.track(metrics.COMMIT, self.account, {"name": "received"}) - query = gql( - """ - mutation CommitReceive($receivedInput:CommitReceivedInput!){ - commitReceive(input:$receivedInput) - } - """ - ) - params = { - "receivedInput": { - "sourceApplication": source_application, - "streamId": stream_id, - "commitId": commit_id, - "message": "message", - } - } - - try: - return self.make_request( - query=query, - params=params, - return_type="commitReceive", - parse_response=False, - ) - except Exception as ex: - print(ex.with_traceback) - return False + return super().received(stream_id, commit_id, source_application, message) diff --git a/src/specklepy/api/resources/other_user.py b/src/specklepy/api/resources/other_user.py index a95f3700..23e222d0 100644 --- a/src/specklepy/api/resources/other_user.py +++ b/src/specklepy/api/resources/other_user.py @@ -8,10 +8,10 @@ from specklepy.logging import metrics from specklepy.logging.exceptions import SpeckleException -NAME = "other_user" +from specklepy.core.api.resources.other_user import NAME, Resource as Core_Resource -class Resource(ResourceBase): +class Resource(Core_Resource): """API Access class for other users, that are not the currently active user.""" def __init__(self, account, basepath, client, server_version) -> None: @@ -35,25 +35,8 @@ def get(self, id: str) -> LimitedUser: LimitedUser -- the retrieved profile of another user """ metrics.track(metrics.OTHER_USER, self.account, {"name": "get"}) - query = gql( - """ - query OtherUser($id: String!) { - otherUser(id: $id) { - id - name - bio - company - avatar - verified - role - } - } - """ - ) - - params = {"id": id} - - return self.make_request(query=query, params=params, return_type="otherUser") + + return super().get(id) def search( self, search_query: str, limit: int = 25 @@ -73,103 +56,6 @@ def search( ) metrics.track(metrics.OTHER_USER, self.account, {"name": "search"}) - query = gql( - """ - query UserSearch($search_query: String!, $limit: Int!) { - userSearch(query: $search_query, limit: $limit) { - items { - id - name - bio - company - avatar - verified - } - } - } - """ - ) - params = {"search_query": search_query, "limit": limit} - - return self.make_request( - query=query, params=params, return_type=["userSearch", "items"] - ) - - def activity( - self, - user_id: str, - limit: int = 20, - action_type: Optional[str] = None, - before: Optional[datetime] = None, - after: Optional[datetime] = None, - cursor: Optional[datetime] = None, - ) -> ActivityCollection: - """ - Get the activity from a given stream in an Activity collection. - Step into the activity `items` for the list of activity. + + return super().search(search_query, limit) - Note: all timestamps arguments should be `datetime` of - any tz as they will be converted to UTC ISO format strings - - user_id {str} -- the id of the user to get the activity from - action_type {str} -- filter results to a single action type - (eg: `commit_create` or `commit_receive`) - limit {int} -- max number of Activity items to return - before {datetime} -- latest cutoff for activity - (ie: return all activity _before_ this time) - after {datetime} -- oldest cutoff for activity - (ie: return all activity _after_ this time) - cursor {datetime} -- timestamp cursor for pagination - """ - - query = gql( - """ - query UserActivity( - $user_id: String!, - $action_type: String, - $before:DateTime, - $after: DateTime, - $cursor: DateTime, - $limit: Int - ){ - otherUser(id: $user_id) { - activity( - actionType: $action_type, - before: $before, - after: $after, - cursor: $cursor, - limit: $limit - ) { - totalCount - cursor - items { - actionType - info - userId - streamId - resourceId - resourceType - message - time - } - } - } - } - """ - ) - - params = { - "user_id": user_id, - "limit": limit, - "action_type": action_type, - "before": before.astimezone(timezone.utc).isoformat() if before else before, - "after": after.astimezone(timezone.utc).isoformat() if after else after, - "cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor, - } - - return self.make_request( - query=query, - params=params, - return_type=["otherUser", "activity"], - schema=ActivityCollection, - ) diff --git a/src/specklepy/api/resources/server.py b/src/specklepy/api/resources/server.py index c7566cc4..7f1e8159 100644 --- a/src/specklepy/api/resources/server.py +++ b/src/specklepy/api/resources/server.py @@ -8,10 +8,10 @@ from specklepy.logging import metrics from specklepy.logging.exceptions import GraphQLException -NAME = "server" +from specklepy.core.api.resources.server import NAME, Resource as Core_Resource -class Resource(ResourceBase): +class Resource(Core_Resource): """API Access class for the server""" def __init__(self, account, basepath, client) -> None: @@ -29,71 +29,8 @@ def get(self) -> ServerInfo: dict -- the server info in dictionary form """ metrics.track(metrics.SERVER, self.account, {"name": "get"}) - query = gql( - """ - query Server { - serverInfo { - name - company - description - adminContact - canonicalUrl - version - roles { - name - description - resourceTarget - } - scopes { - name - description - } - authStrategies{ - id - name - icon - } - } - } - """ - ) - - return self.make_request( - query=query, return_type="serverInfo", schema=ServerInfo - ) - def version(self) -> Tuple[Any, ...]: - """Get the server version - - Returns: - the server version in the format (major, minor, patch, (tag, build)) - eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha - """ - # not tracking as it will be called along with other mutations / queries as a check - query = gql( - """ - query Server { - serverInfo { - version - } - } - """ - ) - ver = self.make_request( - query=query, return_type=["serverInfo", "version"], parse_response=False - ) - if isinstance(ver, Exception): - raise GraphQLException( - f"Could not get server version for {self.basepath}", [ver] - ) - - # pylint: disable=consider-using-generator; (list comp is faster) - return tuple( - [ - int(segment) if segment.isdigit() else segment - for segment in re.split(r"\.|-", ver) - ] - ) + return super().get() def apps(self) -> Dict: """Get the apps registered on the server @@ -102,27 +39,8 @@ def apps(self) -> Dict: dict -- a dictionary of apps registered on the server """ metrics.track(metrics.SERVER, self.account, {"name": "apps"}) - query = gql( - """ - query Apps { - apps{ - id - name - description - termsAndConditionsLink - trustByDefault - logo - author { - id - name - avatar - } - } - } - """ - ) - - return self.make_request(query=query, return_type="apps", parse_response=False) + + return super().apps() def create_token(self, name: str, scopes: List[str], lifespan: int) -> str: """Create a personal API token @@ -136,21 +54,8 @@ def create_token(self, name: str, scopes: List[str], lifespan: int) -> str: str -- the new API token. note: this is the only time you'll see the token! """ metrics.track(metrics.SERVER, self.account, {"name": "create_token"}) - query = gql( - """ - mutation TokenCreate($token: ApiTokenCreateInput!) { - apiTokenCreate(token: $token) - } - """ - ) - params = {"token": {"scopes": scopes, "name": name, "lifespan": lifespan}} - - return self.make_request( - query=query, - params=params, - return_type="apiTokenCreate", - parse_response=False, - ) + + return super().create_token(name, scopes, lifespan) def revoke_token(self, token: str) -> bool: """Revokes (deletes) a personal API token @@ -162,18 +67,5 @@ def revoke_token(self, token: str) -> bool: bool -- True if the token was successfully deleted """ metrics.track(metrics.SERVER, self.account, {"name": "revoke_token"}) - query = gql( - """ - mutation TokenRevoke($token: String!) { - apiTokenRevoke(token: $token) - } - """ - ) - params = {"token": token} - - return self.make_request( - query=query, - params=params, - return_type="apiTokenRevoke", - parse_response=False, - ) + + return super().revoke_token(token) diff --git a/src/specklepy/api/resources/stream.py b/src/specklepy/api/resources/stream.py index ef1a8598..3bb6c60e 100644 --- a/src/specklepy/api/resources/stream.py +++ b/src/specklepy/api/resources/stream.py @@ -9,10 +9,10 @@ from specklepy.logging import metrics from specklepy.logging.exceptions import SpeckleException, UnsupportedException -NAME = "stream" +from specklepy.core.api.resources.stream import NAME, Resource as Core_Resource -class Resource(ResourceBase): +class Resource(Core_Resource): """API Access class for streams""" def __init__(self, account, basepath, client, server_version) -> None: @@ -38,55 +38,8 @@ def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream Stream -- the retrieved stream """ metrics.track(metrics.STREAM, self.account, {"name": "get"}) - query = gql( - """ - query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) { - stream(id: $id) { - id - name - role - description - isPublic - createdAt - updatedAt - commentCount - favoritesCount - collaborators { - id - name - role - avatar - } - branches(limit: $branch_limit) { - totalCount - cursor - items { - id - name - description - commits(limit: $commit_limit) { - totalCount - cursor - items { - id - message - authorId - createdAt - authorName - referencedObject - sourceApplication - } - } - } - } - } - } - """ - ) - - params = {"id": id, "branch_limit": branch_limit, "commit_limit": commit_limit} - return self.make_request(query=query, params=params, return_type="stream") + return super().get(id, branch_limit, commit_limit) def list(self, stream_limit: int = 10) -> List[Stream]: """Get a list of the user's streams @@ -98,49 +51,8 @@ def list(self, stream_limit: int = 10) -> List[Stream]: List[Stream] -- A list of Stream objects """ metrics.track(metrics.STREAM, self.account, {"name": "get"}) - query = gql( - """ - query User($stream_limit: Int!) { - user { - id - bio - name - email - avatar - company - verified - profiles - role - streams(limit: $stream_limit) { - totalCount - cursor - items { - id - name - role - isPublic - createdAt - updatedAt - description - commentCount - favoritesCount - collaborators { - id - name - role - } - } - } - } - } - """ - ) - params = {"stream_limit": stream_limit} - - return self.make_request( - query=query, params=params, return_type=["user", "streams", "items"] - ) + return super().list(stream_limit) def create( self, @@ -160,21 +72,8 @@ def create( id {str} -- the id of the newly created stream """ metrics.track(metrics.STREAM, self.account, {"name": "create"}) - query = gql( - """ - mutation StreamCreate($stream: StreamCreateInput!) { - streamCreate(stream: $stream) - } - """ - ) - - params = { - "stream": {"name": name, "description": description, "isPublic": is_public} - } - - return self.make_request( - query=query, params=params, return_type="streamCreate", parse_response=False - ) + + return super().create(name, description, is_public) def update( self, @@ -196,26 +95,8 @@ def update( bool -- whether the stream update was successful """ metrics.track(metrics.STREAM, self.account, {"name": "update"}) - query = gql( - """ - mutation StreamUpdate($stream: StreamUpdateInput!) { - streamUpdate(stream: $stream) - } - """ - ) - - params = { - "id": id, - "name": name, - "description": description, - "isPublic": is_public, - } - # remove None values so graphql doesn't cry - params = {"stream": {k: v for k, v in params.items() if v is not None}} - return self.make_request( - query=query, params=params, return_type="streamUpdate", parse_response=False - ) + return super().update(id, name, description, is_public) def delete(self, id: str) -> bool: """Delete a stream given its id @@ -227,19 +108,8 @@ def delete(self, id: str) -> bool: bool -- whether the deletion was successful """ metrics.track(metrics.STREAM, self.account, {"name": "delete"}) - query = gql( - """ - mutation StreamDelete($id: String!) { - streamDelete(id: $id) - } - """ - ) - - params = {"id": id} - - return self.make_request( - query=query, params=params, return_type="streamDelete", parse_response=False - ) + + return super().delete(id) def search( self, @@ -260,66 +130,8 @@ def search( List[Stream] -- a list of Streams that match the search query """ metrics.track(metrics.STREAM, self.account, {"name": "search"}) - query = gql( - """ - query StreamSearch( - $search_query: String!, - $limit: Int!, - $branch_limit:Int!, - $commit_limit:Int! - ) { - streams(query: $search_query, limit: $limit) { - items { - id - name - role - description - isPublic - createdAt - updatedAt - collaborators { - id - name - role - avatar - } - branches(limit: $branch_limit) { - totalCount - cursor - items { - id - name - description - commits(limit: $commit_limit) { - totalCount - cursor - items { - id - referencedObject - message - authorName - authorId - createdAt - } - } - } - } - } - } - } - """ - ) - params = { - "search_query": search_query, - "limit": limit, - "branch_limit": branch_limit, - "commit_limit": commit_limit, - } - - return self.make_request( - query=query, params=params, return_type=["streams", "items"] - ) + return super().search(search_query, limit, branch_limit, commit_limit) def favorite(self, stream_id: str, favorited: bool = True): """Favorite or unfavorite the given stream. @@ -333,27 +145,8 @@ def favorite(self, stream_id: str, favorited: bool = True): Stream -- the stream with its `id`, `name`, and `favoritedDate` """ metrics.track(metrics.STREAM, self.account, {"name": "favorite"}) - query = gql( - """ - mutation StreamFavorite($stream_id: String!, $favorited: Boolean!) { - streamFavorite(streamId: $stream_id, favorited: $favorited) { - id - name - favoritedDate - favoritesCount - } - } - """ - ) - - params = { - "stream_id": stream_id, - "favorited": favorited, - } - - return self.make_request( - query=query, params=params, return_type=["streamFavorite"] - ) + + return super().favorite(stream_id, favorited) @deprecated( version="2.6.4", @@ -429,45 +222,8 @@ def get_all_pending_invites( -- a list of pending invites for the specified stream """ metrics.track(metrics.INVITE, self.account, {"name": "get"}) - self._check_invites_supported() - query = gql( - """ - query StreamInvites($streamId: String!) { - stream(id: $streamId){ - pendingCollaborators { - id - token - inviteId - streamId - streamName - title - role - invitedBy{ - id - name - company - avatar - } - user { - id - name - company - avatar - } - } - } - } - """ - ) - params = {"streamId": stream_id} - - return self.make_request( - query=query, - params=params, - return_type=["stream", "pendingCollaborators"], - schema=PendingStreamCollaborator, - ) + return super().get_all_pending_invites(stream_id) def invite( self, @@ -494,37 +250,8 @@ def invite( bool -- True if the operation was successful """ metrics.track(metrics.INVITE, self.account, {"name": "create"}) - self._check_invites_supported() - - if email is None and user_id is None: - raise SpeckleException( - "You must provide either an email or a user id to use the" - " `stream.invite` method" - ) - - query = gql( - """ - mutation StreamInviteCreate($input: StreamInviteCreateInput!) { - streamInviteCreate(input: $input) - } - """ - ) - - params = { - "email": email, - "userId": user_id, - "streamId": stream_id, - "message": message, - "role": role, - } - params = {"input": {k: v for k, v in params.items() if v is not None}} - - return self.make_request( - query=query, - params=params, - return_type="streamInviteCreate", - parse_response=False, - ) + + return super().invite(stream_id, email, user_id, role, message) def invite_batch( self, @@ -550,42 +277,8 @@ def invite_batch( bool -- True if the operation was successful """ metrics.track(metrics.INVITE, self.account, {"name": "batch create"}) - self._check_invites_supported() - if emails is None and user_ids is None: - raise SpeckleException( - "You must provide either an email or a user id to use the" - " `stream.invite` method" - ) - - query = gql( - """ - mutation StreamInviteBatchCreate($input: [StreamInviteCreateInput!]!) { - streamInviteBatchCreate(input: $input) - } - """ - ) - - email_invites = [ - {"streamId": stream_id, "message": message, "email": email} - for email in (emails if emails is not None else []) - if email is not None - ] - - user_invites = [ - {"streamId": stream_id, "message": message, "userId": user_id} - for user_id in (user_ids if user_ids is not None else []) - if user_id is not None - ] - - params = {"input": [*email_invites, *user_invites]} - - return self.make_request( - query=query, - params=params, - return_type="streamInviteBatchCreate", - parse_response=False, - ) + return super().invite_batch(stream_id, emails, user_ids, message) def invite_cancel(self, stream_id: str, invite_id: str) -> bool: """Cancel an existing stream invite @@ -600,24 +293,8 @@ def invite_cancel(self, stream_id: str, invite_id: str) -> bool: bool -- true if the operation was successful """ metrics.track(metrics.INVITE, self.account, {"name": "cancel"}) - self._check_invites_supported() - query = gql( - """ - mutation StreamInviteCancel($streamId: String!, $inviteId: String!) { - streamInviteCancel(streamId: $streamId, inviteId: $inviteId) - } - """ - ) - - params = {"streamId": stream_id, "inviteId": invite_id} - - return self.make_request( - query=query, - params=params, - return_type="streamInviteCancel", - parse_response=False, - ) + return super().invite_cancel(stream_id, invite_id) def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool: """Accept or decline a stream invite @@ -634,28 +311,8 @@ def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool: bool -- true if the operation was successful """ metrics.track(metrics.INVITE, self.account, {"name": "use"}) - self._check_invites_supported() - - query = gql( - """ - mutation StreamInviteUse( - $accept: Boolean!, - $streamId: String!, - $token: String! - ) { - streamInviteUse(accept: $accept, streamId: $streamId, token: $token) - } - """ - ) - - params = {"streamId": stream_id, "token": token, "accept": accept} - - return self.make_request( - query=query, - params=params, - return_type="streamInviteUse", - parse_response=False, - ) + + return super().invite_use(stream_id, token, accept) def update_permission(self, stream_id: str, user_id: str, role: str): """Updates permissions for a user on a given stream @@ -673,38 +330,7 @@ def update_permission(self, stream_id: str, user_id: str, role: str): metrics.track( metrics.PERMISSION, self.account, {"name": "update", "role": role} ) - if self.server_version and ( - self.server_version != ("dev",) and self.server_version < (2, 6, 4) - ): - raise UnsupportedException( - "Server mutation `update_permission` is only supported as of Speckle" - " Server v2.6.4. Please update your Speckle Server to use this method" - " or use the `grant_permission` method instead." - ) - query = gql( - """ - mutation StreamUpdatePermission( - $permission_params: StreamUpdatePermissionInput! - ) { - streamUpdatePermission(permissionParams: $permission_params) - } - """ - ) - - params = { - "permission_params": { - "streamId": stream_id, - "userId": user_id, - "role": role, - } - } - - return self.make_request( - query=query, - params=params, - return_type="streamUpdatePermission", - parse_response=False, - ) + return super().update_permission(stream_id, user_id, role) def revoke_permission(self, stream_id: str, user_id: str): """Revoke permissions from a user on a given stream @@ -717,110 +343,6 @@ def revoke_permission(self, stream_id: str, user_id: str): bool -- True if the operation was successful """ metrics.track(metrics.PERMISSION, self.account, {"name": "revoke"}) - query = gql( - """ - mutation StreamRevokePermission( - $permission_params: StreamRevokePermissionInput! - ) { - streamRevokePermission(permissionParams: $permission_params) - } - """ - ) - params = {"permission_params": {"streamId": stream_id, "userId": user_id}} + return super().revoke_permission(stream_id, user_id) - return self.make_request( - query=query, - params=params, - return_type="streamRevokePermission", - parse_response=False, - ) - - def activity( - self, - stream_id: str, - action_type: Optional[str] = None, - limit: int = 20, - before: Optional[datetime] = None, - after: Optional[datetime] = None, - cursor: Optional[datetime] = None, - ): - """ - Get the activity from a given stream in an Activity collection. - Step into the activity `items` for the list of activity. - - Note: all timestamps arguments should be `datetime` of any tz - as they will be converted to UTC ISO format strings - - stream_id {str} -- the id of the stream to get activity from - action_type {str} - -- filter results to a single action type - (eg: `commit_create` or `commit_receive`) - limit {int} -- max number of Activity items to return - before {datetime} - -- latest cutoff for activity (ie: return all activity _before_ this time) - after {datetime} - -- oldest cutoff for activity (ie: return all activity _after_ this time) - cursor {datetime} -- timestamp cursor for pagination - """ - query = gql( - """ - query StreamActivity( - $stream_id: String!, - $action_type: String, - $before:DateTime, - $after: DateTime, - $cursor: DateTime, - $limit: Int - ){ - stream(id: $stream_id) { - activity( - actionType: $action_type, - before: $before, - after: $after, - cursor: $cursor, - limit: $limit - ) { - totalCount - cursor - items { - actionType - info - userId - streamId - resourceId - resourceType - message - time - } - } - } - } - """ - ) - try: - params = { - "stream_id": stream_id, - "limit": limit, - "action_type": action_type, - "before": before.astimezone(timezone.utc).isoformat() - if before - else before, - "after": after.astimezone(timezone.utc).isoformat() if after else after, - "cursor": cursor.astimezone(timezone.utc).isoformat() - if cursor - else cursor, - } - except AttributeError as e: - raise SpeckleException( - "Could not get stream activity - `before`, `after`, and `cursor` must" - " be in `datetime` format if provided", - ValueError(), - ) from e - - return self.make_request( - query=query, - params=params, - return_type=["stream", "activity"], - schema=ActivityCollection, - ) diff --git a/src/specklepy/api/wrapper.py b/src/specklepy/api/wrapper.py index b2074c6c..f1f687b8 100644 --- a/src/specklepy/api/wrapper.py +++ b/src/specklepy/api/wrapper.py @@ -2,12 +2,11 @@ from warnings import warn from specklepy.api.client import SpeckleClient -from specklepy.api.credentials import ( +from specklepy.core.api.credentials import ( Account, get_account_from_token, get_local_accounts, ) -from specklepy.logging import metrics from specklepy.logging.exceptions import SpeckleException, SpeckleWarning from specklepy.transports.server.server import ServerTransport @@ -75,7 +74,6 @@ def __init__(self, url: str) -> None: self.host = parsed.netloc self.use_ssl = parsed.scheme == "https" segments = parsed.path.strip("/").split("/", 3) - metrics.track(metrics.STREAM_WRAPPER, self.get_account()) if not segments or len(segments) < 2: raise SpeckleException( diff --git a/src/specklepy/core/api/__init__.py b/src/specklepy/core/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specklepy/core/api/credentials.py b/src/specklepy/core/api/credentials.py new file mode 100644 index 00000000..60d02de6 --- /dev/null +++ b/src/specklepy/core/api/credentials.py @@ -0,0 +1,152 @@ +import os +from typing import List, Optional + +from pydantic import BaseModel, Field # pylint: disable=no-name-in-module + +from specklepy.api.models import ServerInfo +from specklepy.core.helpers import speckle_path_provider +from specklepy.logging import metrics +from specklepy.logging.exceptions import SpeckleException +from specklepy.transports.sqlite import SQLiteTransport + + +class UserInfo(BaseModel): + name: Optional[str] = None + email: Optional[str] = None + company: Optional[str] = None + id: Optional[str] = None + + +class Account(BaseModel): + isDefault: bool = False + token: Optional[str] = None + refreshToken: Optional[str] = None + serverInfo: ServerInfo = Field(default_factory=ServerInfo) + userInfo: UserInfo = Field(default_factory=UserInfo) + id: Optional[str] = None + + def __repr__(self) -> str: + return ( + f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}," + f" isDefault: {self.isDefault})" + ) + + def __str__(self) -> str: + return self.__repr__() + + @classmethod + def from_token(cls, token: str, server_url: str = None): + acct = cls(token=token) + acct.serverInfo.url = server_url + return acct + + +def get_local_accounts(base_path: Optional[str] = None) -> List[Account]: + """Gets all the accounts present in this environment + + Arguments: + base_path {str} -- custom base path if you are not using the system default + + Returns: + List[Account] -- list of all local accounts or an empty list if + no accounts were found + """ + accounts: List[Account] = [] + try: + account_storage = SQLiteTransport(scope="Accounts", base_path=base_path) + res = account_storage.get_all_objects() + account_storage.close() + if res: + accounts.extend(Account.parse_raw(r[1]) for r in res) + except SpeckleException: + # cannot open SQLiteTransport, probably because of the lack + # of disk write permissions + pass + + json_acct_files = [] + json_path = str(speckle_path_provider.accounts_folder_path()) + try: + os.makedirs(json_path, exist_ok=True) + json_acct_files.extend( + file for file in os.listdir(json_path) if file.endswith(".json") + ) + + except Exception: + # cannot find or get the json account paths + pass + + if json_acct_files: + try: + accounts.extend( + Account.parse_file(os.path.join(json_path, json_file)) + for json_file in json_acct_files + ) + except Exception as ex: + raise SpeckleException( + "Invalid json accounts could not be read. Please fix or remove them.", + ex, + ) from ex + + return accounts + + +def get_default_account(base_path: Optional[str] = None) -> Optional[Account]: + """ + Gets this environment's default account if any. If there is no default, + the first found will be returned and set as default. + Arguments: + base_path {str} -- custom base path if you are not using the system default + + Returns: + Account -- the default account or None if no local accounts were found + """ + accounts = get_local_accounts(base_path=base_path) + if not accounts: + return None + + default = next((acc for acc in accounts if acc.isDefault), None) + if not default: + default = accounts[0] + default.isDefault = True + #metrics.initialise_tracker(default) + + return default + + +def get_account_from_token(token: str, server_url: str = None) -> Account: + """Gets the local account for the token if it exists + Arguments: + token {str} -- the api token + + Returns: + Account -- the local account with this token or a shell account containing + just the token and url if no local account is found + """ + accounts = get_local_accounts() + if not accounts: + return Account.from_token(token, server_url) + + acct = next((acc for acc in accounts if acc.token == token), None) + if acct: + return acct + + if server_url: + url = server_url.lower() + acct = next( + (acc for acc in accounts if url in acc.serverInfo.url.lower()), None + ) + if acct: + return acct + + return Account.from_token(token, server_url) + + +class StreamWrapper: + def __init__(self, url: str = None) -> None: + raise SpeckleException( + message=( + "The StreamWrapper has moved as of v2.6.0! Please import from" + " specklepy.api.wrapper" + ), + exception=DeprecationWarning(), + ) diff --git a/src/specklepy/core/api/operations.py b/src/specklepy/core/api/operations.py new file mode 100644 index 00000000..a09bc48d --- /dev/null +++ b/src/specklepy/core/api/operations.py @@ -0,0 +1,139 @@ +from typing import List, Optional + +#from specklepy.logging import metrics +from specklepy.logging.exceptions import SpeckleException +from specklepy.objects.base import Base +from specklepy.serialization.base_object_serializer import BaseObjectSerializer +from specklepy.transports.abstract_transport import AbstractTransport +from specklepy.transports.sqlite import SQLiteTransport + + +def send( + base: Base, + transports: Optional[List[AbstractTransport]] = None, + use_default_cache: bool = True, +): + """Sends an object via the provided transports. Defaults to the local cache. + + Arguments: + obj {Base} -- the object you want to send + transports {list} -- where you want to send them + use_default_cache {bool} -- toggle for the default cache. + If set to false, it will only send to the provided transports + + Returns: + str -- the object id of the sent object + """ + + if not transports and not use_default_cache: + raise SpeckleException( + message=( + "You need to provide at least one transport: cannot send with an empty" + " transport list and no default cache" + ) + ) + + if isinstance(transports, AbstractTransport): + transports = [transports] + + if transports is None: + transports = [] + + if use_default_cache: + transports.insert(0, SQLiteTransport()) + + serializer = BaseObjectSerializer(write_transports=transports) + + obj_hash, _ = serializer.write_json(base=base) + + return obj_hash + + +def receive( + obj_id: str, + remote_transport: Optional[AbstractTransport] = None, + local_transport: Optional[AbstractTransport] = None, +) -> Base: + """Receives an object from a transport. + + Arguments: + obj_id {str} -- the id of the object to receive + remote_transport {Transport} -- the transport to receive from + local_transport {Transport} -- the local cache to check for existing objects + (defaults to `SQLiteTransport`) + + Returns: + Base -- the base object + """ + if not local_transport: + local_transport = SQLiteTransport() + + serializer = BaseObjectSerializer(read_transport=local_transport) + + # try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport + obj_string = local_transport.get_object(obj_id) + if obj_string: + return serializer.read_json(obj_string=obj_string) + + if not remote_transport: + raise SpeckleException( + message=( + "Could not find the specified object using the local transport, and you" + " didn't provide a fallback remote from which to pull it." + ) + ) + + obj_string = remote_transport.copy_object_and_children( + id=obj_id, target_transport=local_transport + ) + + return serializer.read_json(obj_string=obj_string) + + +def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str: + """ + Serialize a base object. If no write transports are provided, + the object will be serialized + without detaching or chunking any of the attributes. + + Arguments: + base {Base} -- the object to serialize + write_transports {List[AbstractTransport]} + -- optional: the transports to write to + + Returns: + str -- the serialized object + """ + serializer = BaseObjectSerializer(write_transports=write_transports) + + return serializer.write_json(base)[1] + + +def deserialize( + obj_string: str, read_transport: Optional[AbstractTransport] = None +) -> Base: + """ + Deserialize a string object into a Base object. + + If the object contains referenced child objects that are not stored in the local db, + a read transport needs to be provided in order to recompose + the base with the children objects. + + Arguments: + obj_string {str} -- the string object to deserialize + read_transport {AbstractTransport} + -- the transport to fetch children objects from + (defaults to SQLiteTransport) + + Returns: + Base -- the deserialized object + """ + if not read_transport: + read_transport = SQLiteTransport() + + serializer = BaseObjectSerializer(read_transport=read_transport) + + return serializer.read_json(obj_string=obj_string) + + +__all__ = ["receive", "send", "serialize", "deserialize"] diff --git a/src/specklepy/core/api/resources/__init__.py b/src/specklepy/core/api/resources/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/specklepy/core/api/resources/active_user.py b/src/specklepy/core/api/resources/active_user.py new file mode 100644 index 00000000..59673e59 --- /dev/null +++ b/src/specklepy/core/api/resources/active_user.py @@ -0,0 +1,264 @@ +from datetime import datetime, timezone +from typing import List, Optional + +from gql import gql + +from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User +from specklepy.api.resource import ResourceBase +from specklepy.logging.exceptions import SpeckleException + +NAME = "active_user" + + +class Resource(ResourceBase): + """API Access class for users""" + + def __init__(self, account, basepath, client, server_version) -> None: + super().__init__( + account=account, + basepath=basepath, + client=client, + name=NAME, + server_version=server_version, + ) + self.schema = User + + def get(self) -> User: + """Gets the profile of a user. If no id argument is provided, + will return the current authenticated user's profile + (as extracted from the authorization header). + + Arguments: + id {str} -- the user id + + Returns: + User -- the retrieved user + """ + query = gql( + """ + query User { + activeUser { + id + email + name + bio + company + avatar + verified + profiles + role + } + } + """ + ) + + params = {} + + return self.make_request(query=query, params=params, return_type="activeUser") + + def update( + self, + name: Optional[str] = None, + company: Optional[str] = None, + bio: Optional[str] = None, + avatar: Optional[str] = None, + ): + """Updates your user profile. All arguments are optional. + + Arguments: + name {str} -- your name + company {str} -- the company you may or may not work for + bio {str} -- tell us about yourself + avatar {str} -- a nice photo of yourself + + Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT): + bool -- True if your profile was updated successfully + """ + query = gql( + """ + mutation UserUpdate($user: UserUpdateInput!) { + userUpdate(user: $user) + } + """ + ) + params = {"name": name, "company": company, "bio": bio, "avatar": avatar} + + params = {"user": {k: v for k, v in params.items() if v is not None}} + + if not params["user"]: + return SpeckleException( + message=( + "You must provide at least one field to update your user profile" + ) + ) + + return self.make_request( + query=query, params=params, return_type="userUpdate", parse_response=False + ) + + def activity( + self, + limit: int = 20, + action_type: Optional[str] = None, + before: Optional[datetime] = None, + after: Optional[datetime] = None, + cursor: Optional[datetime] = None, + ): + """ + Get the activity from a given stream in an Activity collection. + Step into the activity `items` for the list of activity. + If no id argument is provided, will return the current authenticated user's + activity (as extracted from the authorization header). + + Note: all timestamps arguments should be `datetime` of any tz as they will be + converted to UTC ISO format strings + + user_id {str} -- the id of the user to get the activity from + action_type {str} -- filter results to a single action type + (eg: `commit_create` or `commit_receive`) + limit {int} -- max number of Activity items to return + before {datetime} -- latest cutoff for activity + (ie: return all activity _before_ this time) + after {datetime} -- oldest cutoff for activity + (ie: return all activity _after_ this time) + cursor {datetime} -- timestamp cursor for pagination + """ + + query = gql( + """ + query UserActivity( + $action_type: String, + $before:DateTime, + $after: DateTime, + $cursor: DateTime, + $limit: Int + ){ + activeUser { + activity( + actionType: $action_type, + before: $before, + after: $after, + cursor: $cursor, + limit: $limit + ) { + totalCount + cursor + items { + actionType + info + userId + streamId + resourceId + resourceType + message + time + } + } + } + } + """ + ) + + params = { + "limit": limit, + "action_type": action_type, + "before": before.astimezone(timezone.utc).isoformat() if before else before, + "after": after.astimezone(timezone.utc).isoformat() if after else after, + "cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor, + } + + return self.make_request( + query=query, + params=params, + return_type=["activeUser", "activity"], + schema=ActivityCollection, + ) + + def get_all_pending_invites(self) -> List[PendingStreamCollaborator]: + """Get all of the active user's pending stream invites + + Requires Speckle Server version >= 2.6.4 + + Returns: + List[PendingStreamCollaborator] + -- a list of pending invites for the current user + """ + self._check_invites_supported() + + query = gql( + """ + query StreamInvites { + streamInvites{ + id + token + inviteId + streamId + streamName + title + role + invitedBy { + id + name + company + avatar + } + } + } + """ + ) + + return self.make_request( + query=query, + return_type="streamInvites", + schema=PendingStreamCollaborator, + ) + + def get_pending_invite( + self, stream_id: str, token: Optional[str] = None + ) -> Optional[PendingStreamCollaborator]: + """Get a particular pending invite for the active user on a given stream. + If no invite_id is provided, any valid invite will be returned. + + Requires Speckle Server version >= 2.6.4 + + Arguments: + stream_id {str} -- the id of the stream to look for invites on + token {str} -- the token of the invite to look for (optional) + + Returns: + PendingStreamCollaborator + -- the invite for the given stream (or None if it isn't found) + """ + self._check_invites_supported() + + query = gql( + """ + query StreamInvite($streamId: String!, $token: String) { + streamInvite(streamId: $streamId, token: $token) { + id + token + streamId + streamName + title + role + invitedBy { + id + name + company + avatar + } + } + } + """ + ) + + params = {"streamId": stream_id} + if token: + params["token"] = token + + return self.make_request( + query=query, + params=params, + return_type="streamInvite", + schema=PendingStreamCollaborator, + ) diff --git a/src/specklepy/core/api/resources/branch.py b/src/specklepy/core/api/resources/branch.py new file mode 100644 index 00000000..167fa13c --- /dev/null +++ b/src/specklepy/core/api/resources/branch.py @@ -0,0 +1,219 @@ +from typing import Optional + +from gql import gql + +from specklepy.api.models import Branch +from specklepy.api.resource import ResourceBase + +NAME = "branch" + + +class Resource(ResourceBase): + """API Access class for branches""" + + def __init__(self, account, basepath, client) -> None: + super().__init__( + account=account, + basepath=basepath, + client=client, + name=NAME, + ) + self.schema = Branch + + def create( + self, stream_id: str, name: str, description: str = "No description provided" + ) -> str: + """Create a new branch on this stream + + Arguments: + name {str} -- the name of the new branch + description {str} -- a short description of the branch + + Returns: + id {str} -- the newly created branch's id + """ + query = gql( + """ + mutation BranchCreate($branch: BranchCreateInput!) { + branchCreate(branch: $branch) + } + """ + ) + params = { + "branch": { + "streamId": stream_id, + "name": name, + "description": description, + } + } + + return self.make_request( + query=query, params=params, return_type="branchCreate", parse_response=False + ) + + def get(self, stream_id: str, name: str, commits_limit: int = 10): + """Get a branch by name from a stream + + Arguments: + stream_id {str} -- the id of the stream to get the branch from + name {str} -- the name of the branch to get + commits_limit {int} -- maximum number of commits to get + + Returns: + Branch -- the fetched branch with its latest commits + """ + query = gql( + """ + query BranchGet($stream_id: String!, $name: String!, $commits_limit: Int!) { + stream(id: $stream_id) { + branch(name: $name) { + id, + name, + description, + commits (limit: $commits_limit) { + totalCount, + cursor, + items { + id, + referencedObject, + sourceApplication, + totalChildrenCount, + message, + authorName, + authorId, + branchName, + parents, + createdAt + } + } + } + } + } + """ + ) + + params = {"stream_id": stream_id, "name": name, "commits_limit": commits_limit} + + return self.make_request( + query=query, params=params, return_type=["stream", "branch"] + ) + + def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10): + """Get a list of branches from a given stream + + Arguments: + stream_id {str} -- the id of the stream to get the branches from + branches_limit {int} -- maximum number of branches to get + commits_limit {int} -- maximum number of commits to get + + Returns: + List[Branch] -- the branches on the stream + """ + query = gql( + """ + query BranchesGet( + $stream_id: String!, + $branches_limit: Int!, + $commits_limit: Int! + ) { + stream(id: $stream_id) { + branches(limit: $branches_limit) { + items { + id + name + description + commits(limit: $commits_limit) { + totalCount + items{ + id + message + referencedObject + sourceApplication + parents + authorId + authorName + branchName + createdAt + } + } + } + } + } + } + """ + ) + + params = { + "stream_id": stream_id, + "branches_limit": branches_limit, + "commits_limit": commits_limit, + } + + return self.make_request( + query=query, params=params, return_type=["stream", "branches", "items"] + ) + + def update( + self, + stream_id: str, + branch_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + ): + """Update a branch + + Arguments: + stream_id {str} -- the id of the stream containing the branch to update + branch_id {str} -- the id of the branch to update + name {str} -- optional: the updated branch name + description {str} -- optional: the updated branch description + + Returns: + bool -- True if update is successful + """ + query = gql( + """ + mutation BranchUpdate($branch: BranchUpdateInput!) { + branchUpdate(branch: $branch) + } + """ + ) + params = { + "branch": { + "streamId": stream_id, + "id": branch_id, + } + } + + if name: + params["branch"]["name"] = name + if description: + params["branch"]["description"] = description + + return self.make_request( + query=query, params=params, return_type="branchUpdate", parse_response=False + ) + + def delete(self, stream_id: str, branch_id: str): + """Delete a branch + + Arguments: + stream_id {str} -- the id of the stream containing the branch to delete + branch_id {str} -- the branch to delete + + Returns: + bool -- True if deletion is successful + """ + query = gql( + """ + mutation BranchDelete($branch: BranchDeleteInput!) { + branchDelete(branch: $branch) + } + """ + ) + + params = {"branch": {"streamId": stream_id, "id": branch_id}} + + return self.make_request( + query=query, params=params, return_type="branchDelete", parse_response=False + ) diff --git a/src/specklepy/core/api/resources/commit.py b/src/specklepy/core/api/resources/commit.py new file mode 100644 index 00000000..a7fb20d2 --- /dev/null +++ b/src/specklepy/core/api/resources/commit.py @@ -0,0 +1,237 @@ +from typing import List, Optional + +from gql import gql + +from specklepy.api.models import Commit +from specklepy.api.resource import ResourceBase + +NAME = "commit" + + +class Resource(ResourceBase): + """API Access class for commits""" + + def __init__(self, account, basepath, client) -> None: + super().__init__( + account=account, + basepath=basepath, + client=client, + name=NAME, + ) + self.schema = Commit + + def get(self, stream_id: str, commit_id: str) -> Commit: + """ + Gets a commit given a stream and the commit id + + Arguments: + stream_id {str} -- the stream where we can find the commit + commit_id {str} -- the id of the commit you want to get + + Returns: + Commit -- the retrieved commit object + """ + query = gql( + """ + query Commit($stream_id: String!, $commit_id: String!) { + stream(id: $stream_id) { + commit(id: $commit_id) { + id + message + referencedObject + authorId + authorName + authorAvatar + branchName + createdAt + sourceApplication + totalChildrenCount + parents + } + } + } + """ + ) + params = {"stream_id": stream_id, "commit_id": commit_id} + + return self.make_request( + query=query, params=params, return_type=["stream", "commit"] + ) + + def list(self, stream_id: str, limit: int = 10) -> List[Commit]: + """ + Get a list of commits on a given stream + + Arguments: + stream_id {str} -- the stream where the commits are + limit {int} -- the maximum number of commits to fetch (default = 10) + + Returns: + List[Commit] -- a list of the most recent commit objects + """ + query = gql( + """ + query Commits($stream_id: String!, $limit: Int!) { + stream(id: $stream_id) { + commits(limit: $limit) { + items { + id + message + referencedObject + authorName + authorId + authorName + authorAvatar + branchName + createdAt + sourceApplication + totalChildrenCount + parents + } + } + } + } + """ + ) + params = {"stream_id": stream_id, "limit": limit} + + return self.make_request( + query=query, params=params, return_type=["stream", "commits", "items"] + ) + + def create( + self, + stream_id: str, + object_id: str, + branch_name: str = "main", + message: str = "", + source_application: str = "python", + parents: List[str] = None, + ) -> str: + """ + Creates a commit on a branch + + Arguments: + stream_id {str} -- the stream you want to commit to + object_id {str} -- the hash of your commit object + branch_name {str} + -- the name of the branch to commit to (defaults to "main") + message {str} + -- optional: a message to give more information about the commit + source_application{str} + -- optional: the application from which the commit was created + (defaults to "python") + parents {List[str]} -- optional: the id of the parent commits + + Returns: + str -- the id of the created commit + """ + query = gql( + """ + mutation CommitCreate ($commit: CommitCreateInput!) + { commitCreate(commit: $commit)} + """ + ) + params = { + "commit": { + "streamId": stream_id, + "branchName": branch_name, + "objectId": object_id, + "message": message, + "sourceApplication": source_application, + } + } + if parents: + params["commit"]["parents"] = parents + + return self.make_request( + query=query, params=params, return_type="commitCreate", parse_response=False + ) + + def update(self, stream_id: str, commit_id: str, message: str) -> bool: + """ + Update a commit + + Arguments: + stream_id {str} + -- the id of the stream that contains the commit you'd like to update + commit_id {str} -- the id of the commit you'd like to update + message {str} -- the updated commit message + + Returns: + bool -- True if the operation succeeded + """ + query = gql( + """ + mutation CommitUpdate($commit: CommitUpdateInput!) + { commitUpdate(commit: $commit)} + """ + ) + params = { + "commit": {"streamId": stream_id, "id": commit_id, "message": message} + } + + return self.make_request( + query=query, params=params, return_type="commitUpdate", parse_response=False + ) + + def delete(self, stream_id: str, commit_id: str) -> bool: + """ + Delete a commit + + Arguments: + stream_id {str} + -- the id of the stream that contains the commit you'd like to delete + commit_id {str} -- the id of the commit you'd like to delete + + Returns: + bool -- True if the operation succeeded + """ + query = gql( + """ + mutation CommitDelete($commit: CommitDeleteInput!) + { commitDelete(commit: $commit)} + """ + ) + params = {"commit": {"streamId": stream_id, "id": commit_id}} + + return self.make_request( + query=query, params=params, return_type="commitDelete", parse_response=False + ) + + def received( + self, + stream_id: str, + commit_id: str, + source_application: str = "python", + message: Optional[str] = None, + ) -> bool: + """ + Mark a commit object a received by the source application. + """ + query = gql( + """ + mutation CommitReceive($receivedInput:CommitReceivedInput!){ + commitReceive(input:$receivedInput) + } + """ + ) + params = { + "receivedInput": { + "sourceApplication": source_application, + "streamId": stream_id, + "commitId": commit_id, + "message": "message", + } + } + + try: + return self.make_request( + query=query, + params=params, + return_type="commitReceive", + parse_response=False, + ) + except Exception as ex: + print(ex.with_traceback) + return False diff --git a/src/specklepy/core/api/resources/other_user.py b/src/specklepy/core/api/resources/other_user.py new file mode 100644 index 00000000..890b0ffb --- /dev/null +++ b/src/specklepy/core/api/resources/other_user.py @@ -0,0 +1,172 @@ +from datetime import datetime, timezone +from typing import List, Optional, Union + +from gql import gql + +from specklepy.api.models import ActivityCollection, LimitedUser +from specklepy.api.resource import ResourceBase +from specklepy.logging.exceptions import SpeckleException + +NAME = "other_user" + + +class Resource(ResourceBase): + """API Access class for other users, that are not the currently active user.""" + + def __init__(self, account, basepath, client, server_version) -> None: + super().__init__( + account=account, + basepath=basepath, + client=client, + name=NAME, + server_version=server_version, + ) + self.schema = LimitedUser + + def get(self, id: str) -> LimitedUser: + """ + Gets the profile of another user. + + Arguments: + id {str} -- the user id + + Returns: + LimitedUser -- the retrieved profile of another user + """ + query = gql( + """ + query OtherUser($id: String!) { + otherUser(id: $id) { + id + name + bio + company + avatar + verified + role + } + } + """ + ) + + params = {"id": id} + + return self.make_request(query=query, params=params, return_type="otherUser") + + def search( + self, search_query: str, limit: int = 25 + ) -> Union[List[LimitedUser], SpeckleException]: + """Searches for user by name or email. The search query must be at least + 3 characters long + + Arguments: + search_query {str} -- a string to search for + limit {int} -- the maximum number of results to return + Returns: + List[LimitedUser] -- a list of User objects that match the search query + """ + if len(search_query) < 3: + return SpeckleException( + message="User search query must be at least 3 characters" + ) + + query = gql( + """ + query UserSearch($search_query: String!, $limit: Int!) { + userSearch(query: $search_query, limit: $limit) { + items { + id + name + bio + company + avatar + verified + } + } + } + """ + ) + params = {"search_query": search_query, "limit": limit} + + return self.make_request( + query=query, params=params, return_type=["userSearch", "items"] + ) + + def activity( + self, + user_id: str, + limit: int = 20, + action_type: Optional[str] = None, + before: Optional[datetime] = None, + after: Optional[datetime] = None, + cursor: Optional[datetime] = None, + ) -> ActivityCollection: + """ + Get the activity from a given stream in an Activity collection. + Step into the activity `items` for the list of activity. + + Note: all timestamps arguments should be `datetime` of + any tz as they will be converted to UTC ISO format strings + + user_id {str} -- the id of the user to get the activity from + action_type {str} -- filter results to a single action type + (eg: `commit_create` or `commit_receive`) + limit {int} -- max number of Activity items to return + before {datetime} -- latest cutoff for activity + (ie: return all activity _before_ this time) + after {datetime} -- oldest cutoff for activity + (ie: return all activity _after_ this time) + cursor {datetime} -- timestamp cursor for pagination + """ + + query = gql( + """ + query UserActivity( + $user_id: String!, + $action_type: String, + $before:DateTime, + $after: DateTime, + $cursor: DateTime, + $limit: Int + ){ + otherUser(id: $user_id) { + activity( + actionType: $action_type, + before: $before, + after: $after, + cursor: $cursor, + limit: $limit + ) { + totalCount + cursor + items { + actionType + info + userId + streamId + resourceId + resourceType + message + time + } + } + } + } + """ + ) + + params = { + "user_id": user_id, + "limit": limit, + "action_type": action_type, + "before": before.astimezone(timezone.utc).isoformat() if before else before, + "after": after.astimezone(timezone.utc).isoformat() if after else after, + "cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor, + } + + return self.make_request( + query=query, + params=params, + return_type=["otherUser", "activity"], + schema=ActivityCollection, + ) diff --git a/src/specklepy/core/api/resources/server.py b/src/specklepy/core/api/resources/server.py new file mode 100644 index 00000000..d566c40f --- /dev/null +++ b/src/specklepy/core/api/resources/server.py @@ -0,0 +1,174 @@ +import re +from typing import Any, Dict, List, Tuple + +from gql import gql + +from specklepy.api.models import ServerInfo +from specklepy.api.resource import ResourceBase +from specklepy.logging.exceptions import GraphQLException + +NAME = "server" + + +class Resource(ResourceBase): + """API Access class for the server""" + + def __init__(self, account, basepath, client) -> None: + super().__init__( + account=account, + basepath=basepath, + client=client, + name=NAME, + ) + + def get(self) -> ServerInfo: + """Get the server info + + Returns: + dict -- the server info in dictionary form + """ + query = gql( + """ + query Server { + serverInfo { + name + company + description + adminContact + canonicalUrl + version + roles { + name + description + resourceTarget + } + scopes { + name + description + } + authStrategies{ + id + name + icon + } + } + } + """ + ) + + return self.make_request( + query=query, return_type="serverInfo", schema=ServerInfo + ) + + def version(self) -> Tuple[Any, ...]: + """Get the server version + + Returns: + the server version in the format (major, minor, patch, (tag, build)) + eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha + """ + # not tracking as it will be called along with other mutations / queries as a check + query = gql( + """ + query Server { + serverInfo { + version + } + } + """ + ) + ver = self.make_request( + query=query, return_type=["serverInfo", "version"], parse_response=False + ) + if isinstance(ver, Exception): + raise GraphQLException( + f"Could not get server version for {self.basepath}", [ver] + ) + + # pylint: disable=consider-using-generator; (list comp is faster) + return tuple( + [ + int(segment) if segment.isdigit() else segment + for segment in re.split(r"\.|-", ver) + ] + ) + + def apps(self) -> Dict: + """Get the apps registered on the server + + Returns: + dict -- a dictionary of apps registered on the server + """ + query = gql( + """ + query Apps { + apps{ + id + name + description + termsAndConditionsLink + trustByDefault + logo + author { + id + name + avatar + } + } + } + """ + ) + + return self.make_request(query=query, return_type="apps", parse_response=False) + + def create_token(self, name: str, scopes: List[str], lifespan: int) -> str: + """Create a personal API token + + Arguments: + scopes {List[str]} -- the scopes to grant with this token + name {str} -- a name for your new token + lifespan {int} -- duration before the token expires + + Returns: + str -- the new API token. note: this is the only time you'll see the token! + """ + query = gql( + """ + mutation TokenCreate($token: ApiTokenCreateInput!) { + apiTokenCreate(token: $token) + } + """ + ) + params = {"token": {"scopes": scopes, "name": name, "lifespan": lifespan}} + + return self.make_request( + query=query, + params=params, + return_type="apiTokenCreate", + parse_response=False, + ) + + def revoke_token(self, token: str) -> bool: + """Revokes (deletes) a personal API token + + Arguments: + token {str} -- the token to revoke (delete) + + Returns: + bool -- True if the token was successfully deleted + """ + query = gql( + """ + mutation TokenRevoke($token: String!) { + apiTokenRevoke(token: $token) + } + """ + ) + params = {"token": token} + + return self.make_request( + query=query, + params=params, + return_type="apiTokenRevoke", + parse_response=False, + ) diff --git a/src/specklepy/core/api/resources/stream.py b/src/specklepy/core/api/resources/stream.py new file mode 100644 index 00000000..62cc66ae --- /dev/null +++ b/src/specklepy/core/api/resources/stream.py @@ -0,0 +1,751 @@ +from datetime import datetime, timezone +from typing import List, Optional + +from deprecated import deprecated +from gql import gql + +from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, Stream +from specklepy.api.resource import ResourceBase +from specklepy.logging.exceptions import SpeckleException, UnsupportedException + +NAME = "stream" + + +class Resource(ResourceBase): + """API Access class for streams""" + + def __init__(self, account, basepath, client, server_version) -> None: + super().__init__( + account=account, + basepath=basepath, + client=client, + name=NAME, + server_version=server_version, + ) + + self.schema = Stream + + def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream: + """Get the specified stream from the server + + Arguments: + id {str} -- the stream id + branch_limit {int} -- the maximum number of branches to return + commit_limit {int} -- the maximum number of commits to return + + Returns: + Stream -- the retrieved stream + """ + query = gql( + """ + query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) { + stream(id: $id) { + id + name + role + description + isPublic + createdAt + updatedAt + commentCount + favoritesCount + collaborators { + id + name + role + avatar + } + branches(limit: $branch_limit) { + totalCount + cursor + items { + id + name + description + commits(limit: $commit_limit) { + totalCount + cursor + items { + id + message + authorId + createdAt + authorName + referencedObject + sourceApplication + } + } + } + } + } + } + """ + ) + + params = {"id": id, "branch_limit": branch_limit, "commit_limit": commit_limit} + + return self.make_request(query=query, params=params, return_type="stream") + + def list(self, stream_limit: int = 10) -> List[Stream]: + """Get a list of the user's streams + + Arguments: + stream_limit {int} -- The maximum number of streams to return + + Returns: + List[Stream] -- A list of Stream objects + """ + query = gql( + """ + query User($stream_limit: Int!) { + user { + id + bio + name + email + avatar + company + verified + profiles + role + streams(limit: $stream_limit) { + totalCount + cursor + items { + id + name + role + isPublic + createdAt + updatedAt + description + commentCount + favoritesCount + collaborators { + id + name + role + } + } + } + } + } + """ + ) + + params = {"stream_limit": stream_limit} + + return self.make_request( + query=query, params=params, return_type=["user", "streams", "items"] + ) + + def create( + self, + name: str = "Anonymous Python Stream", + description: str = "No description provided", + is_public: bool = True, + ) -> str: + """Create a new stream + + Arguments: + name {str} -- the name of the string + description {str} -- a short description of the stream + is_public {bool} + -- whether or not the stream can be viewed by anyone with the id + + Returns: + id {str} -- the id of the newly created stream + """ + query = gql( + """ + mutation StreamCreate($stream: StreamCreateInput!) { + streamCreate(stream: $stream) + } + """ + ) + + params = { + "stream": {"name": name, "description": description, "isPublic": is_public} + } + + return self.make_request( + query=query, params=params, return_type="streamCreate", parse_response=False + ) + + def update( + self, + id: str, + name: Optional[str] = None, + description: Optional[str] = None, + is_public: Optional[bool] = None, + ) -> bool: + """Update an existing stream + + Arguments: + id {str} -- the id of the stream to be updated + name {str} -- the name of the string + description {str} -- a short description of the stream + is_public {bool} + -- whether or not the stream can be viewed by anyone with the id + + Returns: + bool -- whether the stream update was successful + """ + query = gql( + """ + mutation StreamUpdate($stream: StreamUpdateInput!) { + streamUpdate(stream: $stream) + } + """ + ) + + params = { + "id": id, + "name": name, + "description": description, + "isPublic": is_public, + } + # remove None values so graphql doesn't cry + params = {"stream": {k: v for k, v in params.items() if v is not None}} + + return self.make_request( + query=query, params=params, return_type="streamUpdate", parse_response=False + ) + + def delete(self, id: str) -> bool: + """Delete a stream given its id + + Arguments: + id {str} -- the id of the stream to delete + + Returns: + bool -- whether the deletion was successful + """ + query = gql( + """ + mutation StreamDelete($id: String!) { + streamDelete(id: $id) + } + """ + ) + + params = {"id": id} + + return self.make_request( + query=query, params=params, return_type="streamDelete", parse_response=False + ) + + def search( + self, + search_query: str, + limit: int = 25, + branch_limit: int = 10, + commit_limit: int = 10, + ): + """Search for streams by name, description, or id + + Arguments: + search_query {str} -- a string to search for + limit {int} -- the maximum number of results to return + branch_limit {int} -- the maximum number of branches to return + commit_limit {int} -- the maximum number of commits to return + + Returns: + List[Stream] -- a list of Streams that match the search query + """ + query = gql( + """ + query StreamSearch( + $search_query: String!, + $limit: Int!, + $branch_limit:Int!, + $commit_limit:Int! + ) { + streams(query: $search_query, limit: $limit) { + items { + id + name + role + description + isPublic + createdAt + updatedAt + collaborators { + id + name + role + avatar + } + branches(limit: $branch_limit) { + totalCount + cursor + items { + id + name + description + commits(limit: $commit_limit) { + totalCount + cursor + items { + id + referencedObject + message + authorName + authorId + createdAt + } + } + } + } + } + } + } + """ + ) + + params = { + "search_query": search_query, + "limit": limit, + "branch_limit": branch_limit, + "commit_limit": commit_limit, + } + + return self.make_request( + query=query, params=params, return_type=["streams", "items"] + ) + + def favorite(self, stream_id: str, favorited: bool = True): + """Favorite or unfavorite the given stream. + + Arguments: + stream_id {str} -- the id of the stream to favorite / unfavorite + favorited {bool} + -- whether to favorite (True) or unfavorite (False) the stream + + Returns: + Stream -- the stream with its `id`, `name`, and `favoritedDate` + """ + query = gql( + """ + mutation StreamFavorite($stream_id: String!, $favorited: Boolean!) { + streamFavorite(streamId: $stream_id, favorited: $favorited) { + id + name + favoritedDate + favoritesCount + } + } + """ + ) + + params = { + "stream_id": stream_id, + "favorited": favorited, + } + + return self.make_request( + query=query, params=params, return_type=["streamFavorite"] + ) + + def get_all_pending_invites( + self, stream_id: str + ) -> List[PendingStreamCollaborator]: + """Get all of the pending invites on a stream. + You must be a `stream:owner` to query this. + + Requires Speckle Server version >= 2.6.4 + + Arguments: + stream_id {str} -- the stream id from which to get the pending invites + + Returns: + List[PendingStreamCollaborator] + -- a list of pending invites for the specified stream + """ + self._check_invites_supported() + + query = gql( + """ + query StreamInvites($streamId: String!) { + stream(id: $streamId){ + pendingCollaborators { + id + token + inviteId + streamId + streamName + title + role + invitedBy{ + id + name + company + avatar + } + user { + id + name + company + avatar + } + } + } + } + """ + ) + params = {"streamId": stream_id} + + return self.make_request( + query=query, + params=params, + return_type=["stream", "pendingCollaborators"], + schema=PendingStreamCollaborator, + ) + + def invite( + self, + stream_id: str, + email: Optional[str] = None, + user_id: Optional[str] = None, + role: str = "stream:contributor", # should default be reviewer? + message: Optional[str] = None, + ): + """Invite someone to a stream using either their email or user id + + Requires Speckle Server version >= 2.6.4 + + Arguments: + stream_id {str} -- the id of the stream to invite the user to + email {str} -- the email of the user to invite (use this OR `user_id`) + user_id {str} -- the id of the user to invite (use this OR `email`) + role {str} + -- the role to assign to the user (defaults to `stream:contributor`) + message {str} + -- a message to send along with this invite to the specified user + + Returns: + bool -- True if the operation was successful + """ + self._check_invites_supported() + + if email is None and user_id is None: + raise SpeckleException( + "You must provide either an email or a user id to use the" + " `stream.invite` method" + ) + + query = gql( + """ + mutation StreamInviteCreate($input: StreamInviteCreateInput!) { + streamInviteCreate(input: $input) + } + """ + ) + + params = { + "email": email, + "userId": user_id, + "streamId": stream_id, + "message": message, + "role": role, + } + params = {"input": {k: v for k, v in params.items() if v is not None}} + + return self.make_request( + query=query, + params=params, + return_type="streamInviteCreate", + parse_response=False, + ) + + def invite_batch( + self, + stream_id: str, + emails: Optional[List[str]] = None, + user_ids: Optional[List[None]] = None, + message: Optional[str] = None, + ) -> bool: + """Invite a batch of users to a specified stream. + + Requires Speckle Server version >= 2.6.4 + + Arguments: + stream_id {str} -- the id of the stream to invite the user to + emails {List[str]} + -- the email of the user to invite (use this and/or `user_ids`) + user_id {List[str]} + -- the id of the user to invite (use this and/or `emails`) + message {str} + -- a message to send along with this invite to the specified user + + Returns: + bool -- True if the operation was successful + """ + self._check_invites_supported() + if emails is None and user_ids is None: + raise SpeckleException( + "You must provide either an email or a user id to use the" + " `stream.invite` method" + ) + + query = gql( + """ + mutation StreamInviteBatchCreate($input: [StreamInviteCreateInput!]!) { + streamInviteBatchCreate(input: $input) + } + """ + ) + + email_invites = [ + {"streamId": stream_id, "message": message, "email": email} + for email in (emails if emails is not None else []) + if email is not None + ] + + user_invites = [ + {"streamId": stream_id, "message": message, "userId": user_id} + for user_id in (user_ids if user_ids is not None else []) + if user_id is not None + ] + + + params = {"input": [*email_invites, *user_invites]} + + return self.make_request( + query=query, + params=params, + return_type="streamInviteBatchCreate", + parse_response=False, + ) + + def invite_cancel(self, stream_id: str, invite_id: str) -> bool: + """Cancel an existing stream invite + + Requires Speckle Server version >= 2.6.4 + + Arguments: + stream_id {str} -- the id of the stream invite + invite_id {str} -- the id of the invite to use + + Returns: + bool -- true if the operation was successful + """ + self._check_invites_supported() + + query = gql( + """ + mutation StreamInviteCancel($streamId: String!, $inviteId: String!) { + streamInviteCancel(streamId: $streamId, inviteId: $inviteId) + } + """ + ) + + params = {"streamId": stream_id, "inviteId": invite_id} + + return self.make_request( + query=query, + params=params, + return_type="streamInviteCancel", + parse_response=False, + ) + + def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool: + """Accept or decline a stream invite + + Requires Speckle Server version >= 2.6.4 + + Arguments: + stream_id {str} + -- the id of the stream for which the user has a pending invite + token {str} -- the token of the invite to use + accept {bool} -- whether or not to accept the invite (defaults to True) + + Returns: + bool -- true if the operation was successful + """ + self._check_invites_supported() + + query = gql( + """ + mutation StreamInviteUse( + $accept: Boolean!, + $streamId: String!, + $token: String! + ) { + streamInviteUse(accept: $accept, streamId: $streamId, token: $token) + } + """ + ) + + params = {"streamId": stream_id, "token": token, "accept": accept} + + return self.make_request( + query=query, + params=params, + return_type="streamInviteUse", + parse_response=False, + ) + + def update_permission(self, stream_id: str, user_id: str, role: str): + """Updates permissions for a user on a given stream + + Valid for Speckle Server >=2.6.4 + + Arguments: + stream_id {str} -- the id of the stream to grant permissions to + user_id {str} -- the id of the user to grant permissions for + role {str} -- the role to grant the user + + Returns: + bool -- True if the operation was successful + """ + if self.server_version and ( + self.server_version != ("dev",) and self.server_version < (2, 6, 4) + ): + raise UnsupportedException( + "Server mutation `update_permission` is only supported as of Speckle" + " Server v2.6.4. Please update your Speckle Server to use this method" + " or use the `grant_permission` method instead." + ) + query = gql( + """ + mutation StreamUpdatePermission( + $permission_params: StreamUpdatePermissionInput! + ) { + streamUpdatePermission(permissionParams: $permission_params) + } + """ + ) + + params = { + "permission_params": { + "streamId": stream_id, + "userId": user_id, + "role": role, + } + } + + return self.make_request( + query=query, + params=params, + return_type="streamUpdatePermission", + parse_response=False, + ) + + def revoke_permission(self, stream_id: str, user_id: str): + """Revoke permissions from a user on a given stream + + Arguments: + stream_id {str} -- the id of the stream to revoke permissions from + user_id {str} -- the id of the user to revoke permissions from + + Returns: + bool -- True if the operation was successful + """ + query = gql( + """ + mutation StreamRevokePermission( + $permission_params: StreamRevokePermissionInput! + ) { + streamRevokePermission(permissionParams: $permission_params) + } + """ + ) + + params = {"permission_params": {"streamId": stream_id, "userId": user_id}} + + return self.make_request( + query=query, + params=params, + return_type="streamRevokePermission", + parse_response=False, + ) + + def activity( + self, + stream_id: str, + action_type: Optional[str] = None, + limit: int = 20, + before: Optional[datetime] = None, + after: Optional[datetime] = None, + cursor: Optional[datetime] = None, + ): + """ + Get the activity from a given stream in an Activity collection. + Step into the activity `items` for the list of activity. + + Note: all timestamps arguments should be `datetime` of any tz + as they will be converted to UTC ISO format strings + + stream_id {str} -- the id of the stream to get activity from + action_type {str} + -- filter results to a single action type + (eg: `commit_create` or `commit_receive`) + limit {int} -- max number of Activity items to return + before {datetime} + -- latest cutoff for activity (ie: return all activity _before_ this time) + after {datetime} + -- oldest cutoff for activity (ie: return all activity _after_ this time) + cursor {datetime} -- timestamp cursor for pagination + """ + query = gql( + """ + query StreamActivity( + $stream_id: String!, + $action_type: String, + $before:DateTime, + $after: DateTime, + $cursor: DateTime, + $limit: Int + ){ + stream(id: $stream_id) { + activity( + actionType: $action_type, + before: $before, + after: $after, + cursor: $cursor, + limit: $limit + ) { + totalCount + cursor + items { + actionType + info + userId + streamId + resourceId + resourceType + message + time + } + } + } + } + """ + ) + try: + params = { + "stream_id": stream_id, + "limit": limit, + "action_type": action_type, + "before": before.astimezone(timezone.utc).isoformat() + if before + else before, + "after": after.astimezone(timezone.utc).isoformat() if after else after, + "cursor": cursor.astimezone(timezone.utc).isoformat() + if cursor + else cursor, + } + except AttributeError as e: + raise SpeckleException( + "Could not get stream activity - `before`, `after`, and `cursor` must" + " be in `datetime` format if provided", + ValueError(), + ) from e + + return self.make_request( + query=query, + params=params, + return_type=["stream", "activity"], + schema=ActivityCollection, + ) diff --git a/src/specklepy/transports/server/server.py b/src/specklepy/transports/server/server.py index 7ea2e713..040e0aec 100644 --- a/src/specklepy/transports/server/server.py +++ b/src/specklepy/transports/server/server.py @@ -5,7 +5,7 @@ import requests from specklepy.api.client import SpeckleClient -from specklepy.api.credentials import Account, get_account_from_token +from specklepy.core.api.credentials import Account, get_account_from_token from specklepy.logging.exceptions import SpeckleException, SpeckleWarning from specklepy.transports.abstract_transport import AbstractTransport From a2fd21f5413b98d3b78560da4b5623a75b9fb75a Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Fri, 2 Jun 2023 13:18:20 +0100 Subject: [PATCH 02/12] Metrics renamed to SDK Actions --- src/specklepy/api/client.py | 4 -- src/specklepy/api/credentials.py | 3 +- src/specklepy/api/operations.py | 4 +- src/specklepy/api/resources/active_user.py | 8 ++-- src/specklepy/api/resources/branch.py | 13 ++++--- src/specklepy/api/resources/commit.py | 23 ++++++------ src/specklepy/api/resources/other_user.py | 4 +- src/specklepy/api/resources/server.py | 14 +++---- src/specklepy/api/resources/stream.py | 43 +++++++++++----------- src/specklepy/api/resources/user.py | 10 ++--- src/specklepy/logging/metrics.py | 23 ++++++------ 11 files changed, 74 insertions(+), 75 deletions(-) diff --git a/src/specklepy/api/client.py b/src/specklepy/api/client.py index dd7967d0..1cb98a1e 100644 --- a/src/specklepy/api/client.py +++ b/src/specklepy/api/client.py @@ -60,7 +60,6 @@ class SpeckleClient: USE_SSL = True def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None: - #metrics.track(metrics.CLIENT, custom_props={"name": "create"}) ws_protocol = "ws" http_protocol = "http" @@ -129,7 +128,6 @@ def authenticate_with_token(self, token: str) -> None: token {str} -- an api token """ self.account = get_account_from_token(token, self.url) - #metrics.track(metrics.CLIENT, self.account, {"name": "authenticate with token"}) self._set_up_client() def authenticate_with_account(self, account: Account) -> None: @@ -141,12 +139,10 @@ def authenticate_with_account(self, account: Account) -> None: account {Account} -- the account object which can be found with `get_default_account` or `get_local_accounts` """ - #metrics.track(metrics.CLIENT, account, {"name": "authenticate with account"}) self.account = account self._set_up_client() def _set_up_client(self) -> None: - #metrics.track(metrics.CLIENT, self.account, {"name": "set up client"}) headers = { "Authorization": f"Bearer {self.account.token}", "Content-Type": "application/json", diff --git a/src/specklepy/api/credentials.py b/src/specklepy/api/credentials.py index 4c3454c7..07ce5b44 100644 --- a/src/specklepy/api/credentials.py +++ b/src/specklepy/api/credentials.py @@ -28,11 +28,12 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]: accounts = core_get_local_accounts(base_path) metrics.track( - metrics.ACCOUNTS, + metrics.SDK, next( (acc for acc in accounts if acc.isDefault), accounts[0] if accounts else None, ), + {"name": "Get Local Accounts"} ) return accounts diff --git a/src/specklepy/api/operations.py b/src/specklepy/api/operations.py index 016c0281..39186f51 100644 --- a/src/specklepy/api/operations.py +++ b/src/specklepy/api/operations.py @@ -81,7 +81,7 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str Returns: str -- the serialized object """ - metrics.track(metrics.SERIALIZE) + metrics.track(metrics.SDK, custom_props={"name": "Serialize"}) return core_serialize(base, write_transports) @@ -104,7 +104,7 @@ def deserialize( Returns: Base -- the deserialized object """ - metrics.track(metrics.DESERIALIZE) + metrics.track(metrics.SDK, custom_props={"name": "Deserialize"}) return core_deserialize(obj_string, read_transport) diff --git a/src/specklepy/api/resources/active_user.py b/src/specklepy/api/resources/active_user.py index cbc0ca6d..b1ee30f6 100644 --- a/src/specklepy/api/resources/active_user.py +++ b/src/specklepy/api/resources/active_user.py @@ -35,7 +35,7 @@ def get(self) -> User: Returns: User -- the retrieved user """ - metrics.track(metrics.USER, self.account, {"name": "get"}) + metrics.track(metrics.SDK, custom_props={"name": "User Get"}) return super().get() @@ -57,7 +57,7 @@ def update( Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT): bool -- True if your profile was updated successfully """ - metrics.track(metrics.USER, self.account, {"name": "update"}) + metrics.track(metrics.SDK, self.account, {"name": "User Update"}) return super().update(name, company, bio, avatar) @@ -70,7 +70,7 @@ def get_all_pending_invites(self) -> List[PendingStreamCollaborator]: List[PendingStreamCollaborator] -- a list of pending invites for the current user """ - metrics.track(metrics.INVITE, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Invite Get"}) return super().get_all_pending_invites() @@ -90,6 +90,6 @@ def get_pending_invite( PendingStreamCollaborator -- the invite for the given stream (or None if it isn't found) """ - metrics.track(metrics.INVITE, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Invite Get"}) return super().get_pending_invite(stream_id, token) diff --git a/src/specklepy/api/resources/branch.py b/src/specklepy/api/resources/branch.py index de8b268a..ba7e341b 100644 --- a/src/specklepy/api/resources/branch.py +++ b/src/specklepy/api/resources/branch.py @@ -33,7 +33,7 @@ def create( Returns: id {str} -- the newly created branch's id """ - metrics.track(metrics.BRANCH, self.account, {"name": "create"}) + metrics.track(metrics.SDK, self.account, {"name": "Branch Create"}) return super().create(stream_id, name, description) @@ -48,7 +48,7 @@ def get(self, stream_id: str, name: str, commits_limit: int = 10): Returns: Branch -- the fetched branch with its latest commits """ - metrics.track(metrics.BRANCH, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Branch Get"}) return super().get(stream_id, name, commits_limit) @@ -63,7 +63,7 @@ def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10 Returns: List[Branch] -- the branches on the stream """ - metrics.track(metrics.BRANCH, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Branch Get"}) return super().list(stream_id, branches_limit, commits_limit) @@ -85,8 +85,8 @@ def update( Returns: bool -- True if update is successful """ - metrics.track(metrics.BRANCH, self.account, {"name": "update"}) - + metrics.track(metrics.SDK, self.account, {"name": "Branch Update"}) + return super().update(stream_id, branch_id, name, description) def delete(self, stream_id: str, branch_id: str): @@ -99,5 +99,6 @@ def delete(self, stream_id: str, branch_id: str): Returns: bool -- True if deletion is successful """ - metrics.track(metrics.BRANCH, self.account, {"name": "delete"}) + metrics.track(metrics.SDK, self.account, {"name": "Branch Delete"}) + return super().delete(stream_id, branch_id) diff --git a/src/specklepy/api/resources/commit.py b/src/specklepy/api/resources/commit.py index 96b358ea..34841294 100644 --- a/src/specklepy/api/resources/commit.py +++ b/src/specklepy/api/resources/commit.py @@ -32,8 +32,8 @@ def get(self, stream_id: str, commit_id: str) -> Commit: Returns: Commit -- the retrieved commit object """ - metrics.track(metrics.COMMIT, self.account, {"name": "get"}) - + metrics.track(metrics.SDK, self.account, {"name": "Commit Get"}) + return super().get(stream_id, commit_id) def list(self, stream_id: str, limit: int = 10) -> List[Commit]: @@ -47,8 +47,8 @@ def list(self, stream_id: str, limit: int = 10) -> List[Commit]: Returns: List[Commit] -- a list of the most recent commit objects """ - metrics.track(metrics.COMMIT, self.account, {"name": "list"}) - + metrics.track(metrics.SDK, self.account, {"name": "Commit Get"}) + return super().list(stream_id, limit) def create( @@ -78,8 +78,8 @@ def create( Returns: str -- the id of the created commit """ - metrics.track(metrics.COMMIT, self.account, {"name": "create"}) - + metrics.track(metrics.SDK, self.account, {"name": "Commit Create"}) + return super().create(stream_id, object_id, branch_name, message, source_application, parents) def update(self, stream_id: str, commit_id: str, message: str) -> bool: @@ -95,8 +95,8 @@ def update(self, stream_id: str, commit_id: str, message: str) -> bool: Returns: bool -- True if the operation succeeded """ - metrics.track(metrics.COMMIT, self.account, {"name": "update"}) - + metrics.track(metrics.SDK, self.account, {"name": "Commit Update"}) + return super().update(stream_id, commit_id, message) def delete(self, stream_id: str, commit_id: str) -> bool: @@ -111,8 +111,8 @@ def delete(self, stream_id: str, commit_id: str) -> bool: Returns: bool -- True if the operation succeeded """ - metrics.track(metrics.COMMIT, self.account, {"name": "delete"}) - + metrics.track(metrics.SDK, self.account, {"name": "Commit Delete"}) + return super().delete(stream_id, commit_id) def received( @@ -125,5 +125,6 @@ def received( """ Mark a commit object a received by the source application. """ - metrics.track(metrics.COMMIT, self.account, {"name": "received"}) + metrics.track(metrics.SDK, self.account, {"name": "Commit Received"}) + return super().received(stream_id, commit_id, source_application, message) diff --git a/src/specklepy/api/resources/other_user.py b/src/specklepy/api/resources/other_user.py index 23e222d0..2b2eaa6f 100644 --- a/src/specklepy/api/resources/other_user.py +++ b/src/specklepy/api/resources/other_user.py @@ -34,7 +34,7 @@ def get(self, id: str) -> LimitedUser: Returns: LimitedUser -- the retrieved profile of another user """ - metrics.track(metrics.OTHER_USER, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Other User Get"}) return super().get(id) @@ -55,7 +55,7 @@ def search( message="User search query must be at least 3 characters" ) - metrics.track(metrics.OTHER_USER, self.account, {"name": "search"}) + metrics.track(metrics.SDK, self.account, {"name": "Other User Search"}) return super().search(search_query, limit) diff --git a/src/specklepy/api/resources/server.py b/src/specklepy/api/resources/server.py index 7f1e8159..b1192d99 100644 --- a/src/specklepy/api/resources/server.py +++ b/src/specklepy/api/resources/server.py @@ -28,7 +28,7 @@ def get(self) -> ServerInfo: Returns: dict -- the server info in dictionary form """ - metrics.track(metrics.SERVER, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Server Get"}) return super().get() @@ -38,8 +38,8 @@ def apps(self) -> Dict: Returns: dict -- a dictionary of apps registered on the server """ - metrics.track(metrics.SERVER, self.account, {"name": "apps"}) - + metrics.track(metrics.SDK, self.account, {"name": "Server Apps"}) + return super().apps() def create_token(self, name: str, scopes: List[str], lifespan: int) -> str: @@ -53,8 +53,8 @@ def create_token(self, name: str, scopes: List[str], lifespan: int) -> str: Returns: str -- the new API token. note: this is the only time you'll see the token! """ - metrics.track(metrics.SERVER, self.account, {"name": "create_token"}) - + metrics.track(metrics.SDK, self.account, {"name": "Server Create Token"}) + return super().create_token(name, scopes, lifespan) def revoke_token(self, token: str) -> bool: @@ -66,6 +66,6 @@ def revoke_token(self, token: str) -> bool: Returns: bool -- True if the token was successfully deleted """ - metrics.track(metrics.SERVER, self.account, {"name": "revoke_token"}) - + metrics.track(metrics.SDK, self.account, {"name": "Server Revoke Token"}) + return super().revoke_token(token) diff --git a/src/specklepy/api/resources/stream.py b/src/specklepy/api/resources/stream.py index 3bb6c60e..68b62679 100644 --- a/src/specklepy/api/resources/stream.py +++ b/src/specklepy/api/resources/stream.py @@ -37,7 +37,7 @@ def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream Returns: Stream -- the retrieved stream """ - metrics.track(metrics.STREAM, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Stream Get"}) return super().get(id, branch_limit, commit_limit) @@ -50,7 +50,7 @@ def list(self, stream_limit: int = 10) -> List[Stream]: Returns: List[Stream] -- A list of Stream objects """ - metrics.track(metrics.STREAM, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Stream Get"}) return super().list(stream_limit) @@ -71,8 +71,8 @@ def create( Returns: id {str} -- the id of the newly created stream """ - metrics.track(metrics.STREAM, self.account, {"name": "create"}) - + metrics.track(metrics.SDK, self.account, {"name": "Stream Create"}) + return super().create(name, description, is_public) def update( @@ -94,7 +94,7 @@ def update( Returns: bool -- whether the stream update was successful """ - metrics.track(metrics.STREAM, self.account, {"name": "update"}) + metrics.track(metrics.SDK, self.account, {"name": "Stream Update"}) return super().update(id, name, description, is_public) @@ -107,8 +107,8 @@ def delete(self, id: str) -> bool: Returns: bool -- whether the deletion was successful """ - metrics.track(metrics.STREAM, self.account, {"name": "delete"}) - + metrics.track(metrics.SDK, self.account, {"name": "Stream Delete"}) + return super().delete(id) def search( @@ -129,7 +129,7 @@ def search( Returns: List[Stream] -- a list of Streams that match the search query """ - metrics.track(metrics.STREAM, self.account, {"name": "search"}) + metrics.track(metrics.SDK, self.account, {"name": "Stream Search"}) return super().search(search_query, limit, branch_limit, commit_limit) @@ -144,8 +144,8 @@ def favorite(self, stream_id: str, favorited: bool = True): Returns: Stream -- the stream with its `id`, `name`, and `favoritedDate` """ - metrics.track(metrics.STREAM, self.account, {"name": "favorite"}) - + metrics.track(metrics.SDK, self.account, {"name": "Stream Favorite"}) + return super().favorite(stream_id, favorited) @deprecated( @@ -168,7 +168,7 @@ def grant_permission(self, stream_id: str, user_id: str, role: str): Returns: bool -- True if the operation was successful """ - metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role}) + #metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role}) # we're checking for the actual version info, and if the version is 'dev' we treat it # as an up to date instance if self.server_version and ( @@ -221,7 +221,7 @@ def get_all_pending_invites( List[PendingStreamCollaborator] -- a list of pending invites for the specified stream """ - metrics.track(metrics.INVITE, self.account, {"name": "get"}) + metrics.track(metrics.SDK, self.account, {"name": "Invite Get"}) return super().get_all_pending_invites(stream_id) @@ -249,8 +249,8 @@ def invite( Returns: bool -- True if the operation was successful """ - metrics.track(metrics.INVITE, self.account, {"name": "create"}) - + metrics.track(metrics.SDK, self.account, {"name": "Invite Create"}) + return super().invite(stream_id, email, user_id, role, message) def invite_batch( @@ -276,7 +276,7 @@ def invite_batch( Returns: bool -- True if the operation was successful """ - metrics.track(metrics.INVITE, self.account, {"name": "batch create"}) + metrics.track(metrics.SDK, self.account, {"name": "Invite Batch Create"}) return super().invite_batch(stream_id, emails, user_ids, message) @@ -292,7 +292,7 @@ def invite_cancel(self, stream_id: str, invite_id: str) -> bool: Returns: bool -- true if the operation was successful """ - metrics.track(metrics.INVITE, self.account, {"name": "cancel"}) + metrics.track(metrics.SDK, self.account, {"name": "Invite Cancel"}) return super().invite_cancel(stream_id, invite_id) @@ -310,8 +310,8 @@ def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool: Returns: bool -- true if the operation was successful """ - metrics.track(metrics.INVITE, self.account, {"name": "use"}) - + metrics.track(metrics.SDK, self.account, {"name": "Invite Use"}) + return super().invite_use(stream_id, token, accept) def update_permission(self, stream_id: str, user_id: str, role: str): @@ -327,9 +327,8 @@ def update_permission(self, stream_id: str, user_id: str, role: str): Returns: bool -- True if the operation was successful """ - metrics.track( - metrics.PERMISSION, self.account, {"name": "update", "role": role} - ) + metrics.track(metrics.SDK, self.account, {"name": "Permission Update", "role": role}) + return super().update_permission(stream_id, user_id, role) def revoke_permission(self, stream_id: str, user_id: str): @@ -342,7 +341,7 @@ def revoke_permission(self, stream_id: str, user_id: str): Returns: bool -- True if the operation was successful """ - metrics.track(metrics.PERMISSION, self.account, {"name": "revoke"}) + metrics.track(metrics.SDK, self.account, {"name": "Permission Revoke"}) return super().revoke_permission(stream_id, user_id) diff --git a/src/specklepy/api/resources/user.py b/src/specklepy/api/resources/user.py index e46b1ac2..7f396cd8 100644 --- a/src/specklepy/api/resources/user.py +++ b/src/specklepy/api/resources/user.py @@ -44,7 +44,7 @@ def get(self, id: Optional[str] = None) -> User: Returns: User -- the retrieved user """ - metrics.track(metrics.USER, self.account, {"name": "get"}) + #metrics.track(metrics.USER, self.account, {"name": "get"}) query = gql( """ query User($id: String) { @@ -86,7 +86,7 @@ def search( message="User search query must be at least 3 characters" ) - metrics.track(metrics.USER, self.account, {"name": "search"}) + #metrics.track(metrics.USER, self.account, {"name": "search"}) query = gql( """ query UserSearch($search_query: String!, $limit: Int!) { @@ -128,7 +128,7 @@ def update( Returns: bool -- True if your profile was updated successfully """ - metrics.track(metrics.USER, self.account, {"name": "update"}) + #metrics.track(metrics.USER, self.account, {"name": "update"}) query = gql( """ mutation UserUpdate($user: UserUpdateInput!) { @@ -243,7 +243,7 @@ def get_all_pending_invites(self) -> List[PendingStreamCollaborator]: List[PendingStreamCollaborator] -- a list of pending invites for the current user """ - metrics.track(metrics.INVITE, self.account, {"name": "get"}) + #metrics.track(metrics.INVITE, self.account, {"name": "get"}) self._check_invites_supported() query = gql( @@ -291,7 +291,7 @@ def get_pending_invite( PendingStreamCollaborator -- the invite for the given stream (or None if it isn't found) """ - metrics.track(metrics.INVITE, self.account, {"name": "get"}) + #metrics.track(metrics.INVITE, self.account, {"name": "get"}) self._check_invites_supported() query = gql( diff --git a/src/specklepy/logging/metrics.py b/src/specklepy/logging/metrics.py index e06e4d1e..7e743835 100644 --- a/src/specklepy/logging/metrics.py +++ b/src/specklepy/logging/metrics.py @@ -23,23 +23,24 @@ METRICS_TRACKER = None # actions +SDK = "SDK Actions" RECEIVE = "Receive" SEND = "Send" -STREAM = "Stream Action" -PERMISSION = "Permission Action" -INVITE = "Invite Action" -COMMIT = "Commit Action" + +# not in use since 2.15 +ACCOUNTS = "Get Local Accounts" BRANCH = "Branch Action" -USER = "User Action" +CLIENT = "Speckle Client" +COMMIT = "Commit Action" +DESERIALIZE = "serialization/deserialize" +INVITE = "Invite Action" OTHER_USER = "Other User Action" +PERMISSION = "Permission Action" +SERIALIZE = "serialization/serialize" SERVER = "Server Action" -CLIENT = "Speckle Client" +STREAM = "Stream Action" STREAM_WRAPPER = "Stream Wrapper" - -ACCOUNTS = "Get Local Accounts" - -SERIALIZE = "serialization/serialize" -DESERIALIZE = "serialization/deserialize" +USER = "User Action" def disable(): From 8205180e5dce4a50ee08df1fa8bb8b167e38be13 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Fri, 2 Jun 2023 13:27:25 +0100 Subject: [PATCH 03/12] _untracked_receive referenced from core function --- src/specklepy/api/operations.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/specklepy/api/operations.py b/src/specklepy/api/operations.py index 39186f51..ed649202 100644 --- a/src/specklepy/api/operations.py +++ b/src/specklepy/api/operations.py @@ -8,7 +8,7 @@ from specklepy.transports.sqlite import SQLiteTransport from specklepy.core.api.operations import (send as core_send, - receive as core_receive, + receive as _untracked_receive, serialize as core_serialize, deserialize as core_deserialize) @@ -56,15 +56,7 @@ def receive( Base -- the base object """ metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None)) - return core_receive(obj_id, remote_transport, local_transport) - - -def _untracked_receive( - obj_id: str, - remote_transport: Optional[AbstractTransport] = None, - local_transport: Optional[AbstractTransport] = None, -) -> Base: - return receive(obj_id, remote_transport, local_transport) + return _untracked_receive(obj_id, remote_transport, local_transport) def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str: From 48b98294fb706813b077a710fc35e70c2b7d1f18 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Fri, 2 Jun 2023 19:23:03 +0100 Subject: [PATCH 04/12] remove constant var from a child class --- src/specklepy/api/resources/active_user.py | 1 - src/specklepy/api/resources/branch.py | 1 - src/specklepy/api/resources/commit.py | 1 - src/specklepy/api/resources/object.py | 1 - src/specklepy/api/resources/other_user.py | 1 - src/specklepy/api/resources/server.py | 1 - src/specklepy/api/resources/stream.py | 1 - src/specklepy/api/resources/subscriptions.py | 1 - src/specklepy/api/resources/user.py | 1 - 9 files changed, 9 deletions(-) diff --git a/src/specklepy/api/resources/active_user.py b/src/specklepy/api/resources/active_user.py index b1ee30f6..a41b7d50 100644 --- a/src/specklepy/api/resources/active_user.py +++ b/src/specklepy/api/resources/active_user.py @@ -19,7 +19,6 @@ def __init__(self, account, basepath, client, server_version) -> None: account=account, basepath=basepath, client=client, - name=NAME, server_version=server_version, ) self.schema = User diff --git a/src/specklepy/api/resources/branch.py b/src/specklepy/api/resources/branch.py index ba7e341b..5a68bd1c 100644 --- a/src/specklepy/api/resources/branch.py +++ b/src/specklepy/api/resources/branch.py @@ -17,7 +17,6 @@ def __init__(self, account, basepath, client) -> None: account=account, basepath=basepath, client=client, - name=NAME, ) self.schema = Branch diff --git a/src/specklepy/api/resources/commit.py b/src/specklepy/api/resources/commit.py index 34841294..fd0dada6 100644 --- a/src/specklepy/api/resources/commit.py +++ b/src/specklepy/api/resources/commit.py @@ -17,7 +17,6 @@ def __init__(self, account, basepath, client) -> None: account=account, basepath=basepath, client=client, - name=NAME, ) self.schema = Commit diff --git a/src/specklepy/api/resources/object.py b/src/specklepy/api/resources/object.py index 70e116b5..a110ef2c 100644 --- a/src/specklepy/api/resources/object.py +++ b/src/specklepy/api/resources/object.py @@ -16,7 +16,6 @@ def __init__(self, account, basepath, client) -> None: account=account, basepath=basepath, client=client, - name=NAME, ) self.schema = Base diff --git a/src/specklepy/api/resources/other_user.py b/src/specklepy/api/resources/other_user.py index 2b2eaa6f..db9ee8af 100644 --- a/src/specklepy/api/resources/other_user.py +++ b/src/specklepy/api/resources/other_user.py @@ -19,7 +19,6 @@ def __init__(self, account, basepath, client, server_version) -> None: account=account, basepath=basepath, client=client, - name=NAME, server_version=server_version, ) self.schema = LimitedUser diff --git a/src/specklepy/api/resources/server.py b/src/specklepy/api/resources/server.py index b1192d99..21947cc7 100644 --- a/src/specklepy/api/resources/server.py +++ b/src/specklepy/api/resources/server.py @@ -19,7 +19,6 @@ def __init__(self, account, basepath, client) -> None: account=account, basepath=basepath, client=client, - name=NAME, ) def get(self) -> ServerInfo: diff --git a/src/specklepy/api/resources/stream.py b/src/specklepy/api/resources/stream.py index 68b62679..464a2334 100644 --- a/src/specklepy/api/resources/stream.py +++ b/src/specklepy/api/resources/stream.py @@ -20,7 +20,6 @@ def __init__(self, account, basepath, client, server_version) -> None: account=account, basepath=basepath, client=client, - name=NAME, server_version=server_version, ) diff --git a/src/specklepy/api/resources/subscriptions.py b/src/specklepy/api/resources/subscriptions.py index 9651a0fd..c1593326 100644 --- a/src/specklepy/api/resources/subscriptions.py +++ b/src/specklepy/api/resources/subscriptions.py @@ -32,7 +32,6 @@ def __init__(self, account, basepath, client) -> None: account=account, basepath=basepath, client=client, - name=NAME, ) @check_wsclient diff --git a/src/specklepy/api/resources/user.py b/src/specklepy/api/resources/user.py index 7f396cd8..61a468f7 100644 --- a/src/specklepy/api/resources/user.py +++ b/src/specklepy/api/resources/user.py @@ -26,7 +26,6 @@ def __init__(self, account, basepath, client, server_version) -> None: account=account, basepath=basepath, client=client, - name=NAME, server_version=server_version, ) self.schema = User From 869629e2a32527a32157f4f3c342e6239d8714b7 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Fri, 2 Jun 2023 19:31:35 +0100 Subject: [PATCH 05/12] return var for unchanged classes --- src/specklepy/api/resources/object.py | 1 + src/specklepy/api/resources/subscriptions.py | 1 + src/specklepy/api/resources/user.py | 1 + 3 files changed, 3 insertions(+) diff --git a/src/specklepy/api/resources/object.py b/src/specklepy/api/resources/object.py index a110ef2c..70e116b5 100644 --- a/src/specklepy/api/resources/object.py +++ b/src/specklepy/api/resources/object.py @@ -16,6 +16,7 @@ def __init__(self, account, basepath, client) -> None: account=account, basepath=basepath, client=client, + name=NAME, ) self.schema = Base diff --git a/src/specklepy/api/resources/subscriptions.py b/src/specklepy/api/resources/subscriptions.py index c1593326..9651a0fd 100644 --- a/src/specklepy/api/resources/subscriptions.py +++ b/src/specklepy/api/resources/subscriptions.py @@ -32,6 +32,7 @@ def __init__(self, account, basepath, client) -> None: account=account, basepath=basepath, client=client, + name=NAME, ) @check_wsclient diff --git a/src/specklepy/api/resources/user.py b/src/specklepy/api/resources/user.py index 61a468f7..7f396cd8 100644 --- a/src/specklepy/api/resources/user.py +++ b/src/specklepy/api/resources/user.py @@ -26,6 +26,7 @@ def __init__(self, account, basepath, client, server_version) -> None: account=account, basepath=basepath, client=client, + name=NAME, server_version=server_version, ) self.schema = User From d1502c9072c5e094afa25a72ca2e2c881179c4c4 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Fri, 2 Jun 2023 20:03:45 +0100 Subject: [PATCH 06/12] add GIS classes --- src/specklepy/objects/GIS/CRS.py | 22 ++++++ src/specklepy/objects/GIS/__init__.py | 22 ++++++ src/specklepy/objects/GIS/geometry.py | 104 ++++++++++++++++++++++++++ src/specklepy/objects/GIS/layers.py | 96 ++++++++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 src/specklepy/objects/GIS/CRS.py create mode 100644 src/specklepy/objects/GIS/__init__.py create mode 100644 src/specklepy/objects/GIS/geometry.py create mode 100644 src/specklepy/objects/GIS/layers.py diff --git a/src/specklepy/objects/GIS/CRS.py b/src/specklepy/objects/GIS/CRS.py new file mode 100644 index 00000000..cfe15fd9 --- /dev/null +++ b/src/specklepy/objects/GIS/CRS.py @@ -0,0 +1,22 @@ +from typing import Optional +from specklepy.objects import Base + + +class CRS(Base, speckle_type="Objects.GIS.CRS"): + """A Coordinate Reference System stored in wkt format""" + + def __init__( + self, + name: Optional[str] = None, + authority_id: Optional[str] = None, + wkt: Optional[str] = None, + units: Optional[str] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + + self.name = name + self.authority_id = authority_id + self.wkt = wkt + self.units = units or "m" + diff --git a/src/specklepy/objects/GIS/__init__.py b/src/specklepy/objects/GIS/__init__.py new file mode 100644 index 00000000..566eab99 --- /dev/null +++ b/src/specklepy/objects/GIS/__init__.py @@ -0,0 +1,22 @@ +"""Builtin Speckle object kit.""" + +from specklepy.objects.GIS.layers import ( + VectorLayer, + RasterLayer, +) + +from specklepy.objects.GIS.geometry import ( + GisPolygonGeometry, + GisPolygonElement, + GisLineElement, + GisPointElement, + GisRasterElement, +) + +from specklepy.objects.GIS.CRS import ( + CRS, +) + +__all__ = ["VectorLayer", "RasterLayer", + "GisPolygonGeometry", "GisPolygonElement", "GisLineElement", "GisPointElement", "GisRasterElement", + "CRS"] diff --git a/src/specklepy/objects/GIS/geometry.py b/src/specklepy/objects/GIS/geometry.py new file mode 100644 index 00000000..2d66f6cb --- /dev/null +++ b/src/specklepy/objects/GIS/geometry.py @@ -0,0 +1,104 @@ + +from typing import Optional, Union, List +from specklepy.objects.geometry import Point, Line, Polyline, Circle, Arc, Polycurve, Mesh +from specklepy.objects import Base +from deprecated import deprecated + +class GisPolygonGeometry(Base, speckle_type="Objects.GIS.PolygonGeometry", detachable={"displayValue"}): + """GIS Polygon Geometry""" + + def __init__( + self, + boundary: Optional[Union[Polyline, Arc, Line, Circle, Polycurve]] = None, + voids: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]] ] = None, + displayValue: Optional[List[Mesh]] = None, + units: Optional[str] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + + self.boundary = boundary + self.voids = voids + self.displayValue = displayValue + self.units = units or "m" + +class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"): + """GIS Polygon element""" + + def __init__( + self, + geometry: Optional[List[GisPolygonGeometry]] = None, + attributes: Optional[Base] = None, + units: Optional[str] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + + self.geometry = geometry + self.attributes = attributes + self.units = units or "m" + +class GisLineElement(Base, speckle_type="Objects.GIS.LineElement"): + """GIS Polyline element""" + + def __init__( + self, + geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None, + attributes: Optional[Base] = None, + units: Optional[str] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + + self.geometry = geometry + self.attributes = attributes + self.units = units or "m" + +class GisPointElement(Base, speckle_type="Objects.GIS.PointElement"): + """GIS Point element""" + + def __init__( + self, + geometry: Optional[List[Point]] = None, + attributes: Optional[Base] = None, + units: Optional[str] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + + self.geometry = geometry + self.attributes = attributes + self.units = units or "m" + +class GisRasterElement(Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}): + """GIS Raster element""" + + def __init__( + self, + band_count: Optional[int] = None, + band_names: Optional[List[str]] = None, + x_origin: Optional[float] = None, + y_origin: Optional[float] = None, + x_size: Optional[int] = None, + y_size: Optional[int] = None, + x_resolution: Optional[float] = None, + y_resolution: Optional[float] = None, + noDataValue: Optional[List[float]] = None, + displayValue: Optional[List[Mesh]] = None, + units: Optional[str] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + + self.band_count = band_count + self.band_names = band_names + self.x_origin = x_origin + self.y_origin = y_origin + self.x_size = x_size + self.y_size = y_size + self.x_resolution = x_resolution + self.y_resolution = y_resolution + self.noDataValue = noDataValue + self.displayValue = displayValue + self.units = units or "m" + diff --git a/src/specklepy/objects/GIS/layers.py b/src/specklepy/objects/GIS/layers.py new file mode 100644 index 00000000..092316aa --- /dev/null +++ b/src/specklepy/objects/GIS/layers.py @@ -0,0 +1,96 @@ +from typing import Any, Dict, List, Optional +from specklepy.objects.base import Base +from specklepy.objects.other import Collection + +from specklepy.objects.GIS.CRS import CRS +from deprecated import deprecated + +@deprecated(version="2.15", reason="Use VectorLayer or RasterLayer instead") +class Layer(Base, detachable={"features"}): + """A GIS Layer""" + def __init__( + self, + name:str=None, + crs:CRS=None, + units: str = "m", + features: Optional[List[Base]] = None, + layerType: str = "None", + geomType: str = "None", + renderer: Optional[dict[str, Any]] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + self.name = name + self.crs = crs + self.units = units + self.type = layerType + self.features = features or [] + self.geomType = geomType + self.renderer = renderer or {} + +class VectorLayer(Collection, detachable={"elements"}, speckle_type="Objects.GIS.VectorLayer", serialize_ignore={"features"}): + """GIS Vector Layer""" + + def __init__( + self, + name: Optional[str]=None, + crs: Optional[CRS]=None, + units: Optional[str] = None, + elements: Optional[List[Base]] = None, + attributes: Optional[Base] = None, + geomType: Optional[str] = None, + renderer: Optional[Dict[str, Any]] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + self.name = name or "" + self.crs = crs + self.units = units + self.elements = elements or [] + self.attributes = attributes + self.geomType = geomType or "None" + self.renderer = renderer or {} + self.collectionType = "VectorLayer" + + @property + @deprecated(version="2.14", reason="Use elements") + def features(self) -> Optional[List[Base]]: + return self.elements + + @features.setter + def features(self, value: Optional[List[Base]]) -> None: + self.elements = value + +class RasterLayer(Collection, detachable={"elements"}, speckle_type="Objects.GIS.RasterLayer", serialize_ignore={"features"}): + """GIS Raster Layer""" + + def __init__( + self, + name: Optional[str] = None, + crs: Optional[CRS]=None, + units: Optional[str] = None, + rasterCrs: Optional[CRS]=None, + elements: Optional[List[Base]] = None, + geomType: Optional[str] = None, + renderer: Optional[Dict[str, Any]] = None, + **kwargs + ) -> None: + super().__init__(**kwargs) + self.name = name or "" + self.crs = crs + self.units = units + self.rasterCrs = rasterCrs + self.elements = elements or [] + self.geomType = geomType or "None" + self.renderer = renderer or {} + self.collectionType = "RasterLayer" + + @property + @deprecated(version="2.14", reason="Use elements") + def features(self) -> Optional[List[Base]]: + return self.elements + + @features.setter + def features(self, value: Optional[List[Base]]) -> None: + self.elements = value + From 6049049813536bbfc9231431ea4105cc40d35b6a Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Mon, 12 Jun 2023 20:05:47 +0100 Subject: [PATCH 07/12] fixed server_id --- src/specklepy/logging/metrics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/specklepy/logging/metrics.py b/src/specklepy/logging/metrics.py index 7e743835..f6fb5c21 100644 --- a/src/specklepy/logging/metrics.py +++ b/src/specklepy/logging/metrics.py @@ -97,7 +97,7 @@ def initialise_tracker(account=None): if account and account.userInfo.email: METRICS_TRACKER.set_last_user(account.userInfo.email) if account and account.serverInfo.url: - METRICS_TRACKER.set_last_server(account.userInfo.email) + METRICS_TRACKER.set_last_server(account.serverInfo.url) class Singleton(type): @@ -140,7 +140,8 @@ def set_last_server(self, server: str): self.last_server = self.hash(server) def hash(self, value: str): - return hashlib.md5(value.lower().encode("utf-8")).hexdigest().upper() + input = value.lower().replace("https://","") + return hashlib.md5(input.encode("utf-8")).hexdigest().upper() def _send_tracking_requests(self): session = requests.Session() From 06952a59914220afa50a646953d5575a02c8aaaa Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Tue, 13 Jun 2023 12:17:27 +0100 Subject: [PATCH 08/12] more specific domain identification --- src/specklepy/logging/metrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/specklepy/logging/metrics.py b/src/specklepy/logging/metrics.py index f6fb5c21..089c135b 100644 --- a/src/specklepy/logging/metrics.py +++ b/src/specklepy/logging/metrics.py @@ -140,7 +140,8 @@ def set_last_server(self, server: str): self.last_server = self.hash(server) def hash(self, value: str): - input = value.lower().replace("https://","") + inputList = value.lower().split("://") + input = inputList[len(inputList)-1].split("/")[0].split('?')[0] return hashlib.md5(input.encode("utf-8")).hexdigest().upper() def _send_tracking_requests(self): From dcd13224d0ed6463f373ceeabb5301e8e8de86b3 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Tue, 13 Jun 2023 17:48:03 +0100 Subject: [PATCH 09/12] added native_units for Degrees --- src/specklepy/objects/GIS/CRS.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/specklepy/objects/GIS/CRS.py b/src/specklepy/objects/GIS/CRS.py index cfe15fd9..0b5287fc 100644 --- a/src/specklepy/objects/GIS/CRS.py +++ b/src/specklepy/objects/GIS/CRS.py @@ -11,6 +11,7 @@ def __init__( authority_id: Optional[str] = None, wkt: Optional[str] = None, units: Optional[str] = None, + native_units: Optional[str] = None, **kwargs ) -> None: super().__init__(**kwargs) @@ -19,4 +20,5 @@ def __init__( self.authority_id = authority_id self.wkt = wkt self.units = units or "m" + self.native_units = native_units From f456e4ddbbfd558696793e91d8d933af867ba368 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Tue, 13 Jun 2023 17:53:03 +0100 Subject: [PATCH 10/12] rename --- src/specklepy/objects/GIS/CRS.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/specklepy/objects/GIS/CRS.py b/src/specklepy/objects/GIS/CRS.py index 0b5287fc..f9ad7b45 100644 --- a/src/specklepy/objects/GIS/CRS.py +++ b/src/specklepy/objects/GIS/CRS.py @@ -11,7 +11,7 @@ def __init__( authority_id: Optional[str] = None, wkt: Optional[str] = None, units: Optional[str] = None, - native_units: Optional[str] = None, + units_native: Optional[str] = None, **kwargs ) -> None: super().__init__(**kwargs) @@ -20,5 +20,5 @@ def __init__( self.authority_id = authority_id self.wkt = wkt self.units = units or "m" - self.native_units = native_units + self.units_native = units_native From b83c30a1c986615278258c92ab38514607dc7252 Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Wed, 21 Jun 2023 12:45:18 +0100 Subject: [PATCH 11/12] added offsets to crs class --- src/specklepy/objects/GIS/CRS.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/specklepy/objects/GIS/CRS.py b/src/specklepy/objects/GIS/CRS.py index f9ad7b45..2d29e967 100644 --- a/src/specklepy/objects/GIS/CRS.py +++ b/src/specklepy/objects/GIS/CRS.py @@ -12,6 +12,8 @@ def __init__( wkt: Optional[str] = None, units: Optional[str] = None, units_native: Optional[str] = None, + offset_x: Optional[float] = None, + offset_y: Optional[float] = None, **kwargs ) -> None: super().__init__(**kwargs) @@ -21,4 +23,6 @@ def __init__( self.wkt = wkt self.units = units or "m" self.units_native = units_native + self.offset_x = offset_x + self.offset_y = offset_y From ed9f1ad8182123073f240acebc3923b3ed6429ea Mon Sep 17 00:00:00 2001 From: KatKatKateryna Date: Wed, 21 Jun 2023 17:21:50 +0100 Subject: [PATCH 12/12] rotation --- src/specklepy/objects/GIS/CRS.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/specklepy/objects/GIS/CRS.py b/src/specklepy/objects/GIS/CRS.py index 2d29e967..b21b6c58 100644 --- a/src/specklepy/objects/GIS/CRS.py +++ b/src/specklepy/objects/GIS/CRS.py @@ -14,6 +14,7 @@ def __init__( units_native: Optional[str] = None, offset_x: Optional[float] = None, offset_y: Optional[float] = None, + rotation: Optional[float] = None, **kwargs ) -> None: super().__init__(**kwargs) @@ -25,4 +26,5 @@ def __init__( self.units_native = units_native self.offset_x = offset_x self.offset_y = offset_y + self.rotation = rotation