Compare commits

..

16 Commits

Author SHA1 Message Date
izzy lyseggen fc7e3ca893 fix(serializer): wrap traverse_base
moving `begin` and `end_write` to the seriazlier due to the new
sqlite transport with batched writes necessitates a wrapper around
`traverse_base` so end/begin write can be called once at the top level.
just adding begin/end write to the original traversal method would make
tons of calls to `end_write` since the traversal is recursive
2022-06-20 11:48:45 +01:00
izzy lyseggen 9a4635c561 fix(client): don't parse obj create response 2022-06-20 11:42:18 +01:00
izzy lyseggen 9f873333f5 fix(serializer): warn but don't throw if ref not found
this is _not_ an issue with the transports, but an issue with using the
graphql api to fetch objects. since you are only receiving one obj and none of
the children, the transport has no way to find them and should simply
return the reference as is. idk why anyone would really use `object.get`
so tbh i'm not surprised no one has found this bug yet lol
2022-06-20 11:21:09 +01:00
izzy lyseggen 50c78d679e style: formatting 2022-06-20 11:17:32 +01:00
izzy lyseggen 72dcb674eb fix(serialization): move end and begin write 2022-06-20 07:52:58 +01:00
izzy lyseggen 9d038de3ab chore: dev container update 2022-06-20 07:52:27 +01:00
Gergő Jedlicska 52a667f28e add start and finish write method calls to base object serialize 2022-06-19 14:41:22 +02:00
Gergő Jedlicska 31cd12160b update test fixture auth to non deprecated token based method 2022-06-19 14:40:50 +02:00
Gergő Jedlicska 22e7f18648 update to new circleci redis baseimage 2022-06-19 14:40:18 +02:00
Gergő Jedlicska 97815ead9d Merge branch 'gergo/sqliteSpeedup' of github.com:specklesystems/specklepy into gergo/sqliteSpeedup 2022-06-19 14:06:18 +02:00
Gergő Jedlicska 332331465d update CI versions 2022-06-19 14:04:38 +02:00
izzy lyseggen b56f006103 ci: formatting 2022-06-17 17:21:06 +01:00
izzy lyseggen 1fdefc8929 ci: bump node version 2022-06-17 16:50:04 +01:00
izzy lyseggen f51dd28e46 chore: upgrade gql3
also removed py-spy as it's not used and i was getting install errors :/
2022-06-17 16:29:28 +01:00
izzy lyseggen e22bfd72ea feat(transports): batching sqlite inserts 2022-06-17 16:15:47 +01:00
Gergő Jedlicska 739d8bc189 quick and hacky sqlite batching 2022-06-17 16:35:15 +02:00
34 changed files with 886 additions and 1899 deletions
+1 -1
View File
@@ -65,7 +65,7 @@ workflows:
- test: - test:
matrix: matrix:
parameters: parameters:
tag: ["3.7", "3.8", "3.9", "3.10"] tag: ["3.6", "3.7", "3.8", "3.9"]
filters: filters:
tags: tags:
only: /.*/ only: /.*/
+4 -4
View File
@@ -1,11 +1,11 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/.devcontainer/base.Dockerfile # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.191.1/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 # [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6
ARG VARIANT="3.10" ARG VARIANT="3.9"
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT} FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="16" ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
-3
View File
@@ -22,9 +22,6 @@
"python.languageServer": "Pylance", "python.languageServer": "Pylance",
"python.linting.enabled": true, "python.linting.enabled": true,
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,
"python.linting.pylintArgs": [
"--max-line-length=120"
],
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
+7 -1
View File
@@ -6,12 +6,16 @@ services:
POSTGRES_DB: speckle2_test POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle POSTGRES_USER: speckle
# ports:
# - "5432:5432"
network_mode: host network_mode: host
redis: redis:
image: cimg/redis:6.2 image: cimg/redis:6.2
# ports:
# - "6379:6379"
network_mode: host network_mode: host
speckle-server: speckle-server:
image: speckle/speckle-server:latest image: speckle/speckle-server
command: ["bash", "-c", "/wait && node bin/www"] command: ["bash", "-c", "/wait && node bin/www"]
environment: environment:
POSTGRES_URL: "localhost" POSTGRES_URL: "localhost"
@@ -24,6 +28,8 @@ services:
CANONICAL_URL: "http://localhost:3000" CANONICAL_URL: "http://localhost:3000"
WAIT_HOSTS: localhost:5432, localhost:6379 WAIT_HOSTS: localhost:5432, localhost:6379
DISABLE_FILE_UPLOADS: "true" DISABLE_FILE_UPLOADS: "true"
# ports:
# - "3000:3000"
network_mode: host network_mode: host
specklepy: specklepy:
Generated
+457 -520
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -11,11 +11,11 @@ homepage = "https://speckle.systems/"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.7.2, <4.0" python = "^3.6.5"
pydantic = "^1.8.2" pydantic = "^1.8.2"
appdirs = "^1.4.4" appdirs = "^1.4.4"
gql = {extras = ["requests", "websockets"], version = "^3.3.0"} gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
ujson = "^5.3.0" ujson = "^4.3.0"
Deprecated = "^1.2.13" Deprecated = "^1.2.13"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
@@ -25,7 +25,6 @@ pytest = "^6.2.2"
pytest-ordering = "^0.6" pytest-ordering = "^0.6"
pytest-cov = "^3.0.0" pytest-cov = "^3.0.0"
devtools = "^0.8.0" devtools = "^0.8.0"
pylint = "^2.14.4"
[tool.black] [tool.black]
@@ -45,7 +44,7 @@ exclude = '''
''' '''
include = '\.pyi?$' include = '\.pyi?$'
line-length = 88 line-length = 88
target-version = ["py37", "py38", "py39", "py310"] target-version = ["py36", "py37", "py38"]
[build-system] [build-system]
+7 -18
View File
@@ -160,25 +160,8 @@ class SpeckleClient:
return self.httpclient.execute(query) return self.httpclient.execute(query)
def _init_resources(self) -> None: def _init_resources(self) -> None:
self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
try:
server_version = self.server.version()
except:
pass
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource( self.stream = stream.Resource(
account=self.account, account=self.account, basepath=self.url, client=self.httpclient
basepath=self.url,
client=self.httpclient,
server_version=server_version,
) )
self.commit = commit.Resource( self.commit = commit.Resource(
account=self.account, basepath=self.url, client=self.httpclient account=self.account, basepath=self.url, client=self.httpclient
@@ -189,6 +172,12 @@ class SpeckleClient:
self.object = object.Resource( self.object = object.Resource(
account=self.account, basepath=self.url, client=self.httpclient account=self.account, basepath=self.url, client=self.httpclient
) )
self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.user = user.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.subscribe = subscriptions.Resource( self.subscribe = subscriptions.Resource(
account=self.account, account=self.account,
basepath=self.ws_url, basepath=self.ws_url,
+6 -10
View File
@@ -1,5 +1,5 @@
import os import os
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module from pydantic import BaseModel, Field
from typing import List, Optional from typing import List, Optional
from specklepy.logging import metrics from specklepy.logging import metrics
from specklepy.api.models import ServerInfo from specklepy.api.models import ServerInfo
@@ -16,11 +16,11 @@ class UserInfo(BaseModel):
class Account(BaseModel): class Account(BaseModel):
isDefault: bool = False isDefault: bool = False
token: Optional[str] = None token: str = None
refreshToken: Optional[str] = None refreshToken: str = None
serverInfo: ServerInfo = Field(default_factory=ServerInfo) serverInfo: ServerInfo = Field(default_factory=ServerInfo)
userInfo: UserInfo = Field(default_factory=UserInfo) userInfo: UserInfo = Field(default_factory=UserInfo)
id: Optional[str] = None id: str = None
def __repr__(self) -> str: def __repr__(self) -> str:
return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})" return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})"
@@ -45,15 +45,12 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
List[Account] -- list of all local accounts or an empty list if no accounts were found List[Account] -- list of all local accounts or an empty list if no accounts were found
""" """
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path) account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
# pylint: disable=protected-access
json_path = os.path.join(account_storage._base_path, "Accounts") json_path = os.path.join(account_storage._base_path, "Accounts")
os.makedirs(json_path, exist_ok=True) os.makedirs(json_path, exist_ok=True)
json_acct_files = [file for file in os.listdir(json_path) if file.endswith(".json")] json_acct_files = [file for file in os.listdir(json_path) if file.endswith(".json")]
accounts: List[Account] = [] accounts = []
res = account_storage.get_all_objects() res = account_storage.get_all_objects()
account_storage.close()
if res: if res:
accounts.extend(Account.parse_raw(r[1]) for r in res) accounts.extend(Account.parse_raw(r[1]) for r in res)
if json_acct_files: if json_acct_files:
@@ -66,8 +63,7 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
raise SpeckleException( raise SpeckleException(
"Invalid json accounts could not be read. Please fix or remove them.", "Invalid json accounts could not be read. Please fix or remove them.",
ex, ex,
) from ex )
metrics.track( metrics.track(
metrics.ACCOUNTS, metrics.ACCOUNTS,
next( next(
+3 -21
View File
@@ -3,10 +3,10 @@
# timestamp: 2020-11-17T14:33:13+00:00 # timestamp: 2020-11-17T14:33:13+00:00
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import Any, Dict, List, Optional
from pydantic import BaseModel # pylint: disable=no-name-in-module from pydantic import BaseModel
class Collaborator(BaseModel): class Collaborator(BaseModel):
@@ -110,24 +110,6 @@ class User(BaseModel):
return self.__repr__() return self.__repr__()
class PendingStreamCollaborator(BaseModel):
id: Optional[str]
inviteId: Optional[str]
streamId: Optional[str]
streamName: Optional[str]
title: Optional[str]
role: Optional[str]
invitedBy: Optional[User]
user: Optional[User]
token: Optional[str]
def __repr__(self):
return f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId: {self.streamId}, role: {self.role}, title: {self.title}, invitedBy: {self.user.name if self.user else None})"
def __str__(self) -> str:
return self.__repr__()
class Activity(BaseModel): class Activity(BaseModel):
actionType: Optional[str] actionType: Optional[str]
info: Optional[dict] info: Optional[dict]
@@ -151,7 +133,7 @@ class ActivityCollection(BaseModel):
cursor: Optional[datetime] cursor: Optional[datetime]
def __repr__(self) -> str: def __repr__(self) -> str:
return f"ActivityCollection( totalCount: {self.totalCount}, items: {len(self.items) if self.items else 0}, cursor: {self.cursor.isoformat() if self.cursor else None} )" return f"ActivityCollection( totalCount: {self.totalCount}, items: {len(self.items) if self.items else 0}, cursor: {self.cursor.isoformat()} )"
def __str__(self) -> str: def __str__(self) -> str:
return self.__repr__() return self.__repr__()
-3
View File
@@ -29,9 +29,6 @@ def send(
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache" 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: if transports is None:
metrics.track(metrics.SEND) metrics.track(metrics.SEND)
transports = [] transports = []
+20 -52
View File
@@ -1,14 +1,10 @@
from graphql import DocumentNode
from specklepy.api.credentials import Account from specklepy.api.credentials import Account
from specklepy.transports.sqlite import SQLiteTransport from specklepy.transports.sqlite import SQLiteTransport
from typing import Any, Dict, List, Optional, Tuple, Type, Union from typing import Dict, List
from gql.client import Client from gql.client import Client
from gql.gql import gql
from gql.transport.exceptions import TransportQueryError from gql.transport.exceptions import TransportQueryError
from specklepy.logging.exceptions import ( from specklepy.logging.exceptions import GraphQLException, SpeckleException
GraphQLException,
SpeckleException,
UnsupportedException,
)
from specklepy.serialization.base_object_serializer import BaseObjectSerializer from specklepy.serialization.base_object_serializer import BaseObjectSerializer
@@ -19,30 +15,28 @@ class ResourceBase(object):
basepath: str, basepath: str,
client: Client, client: Client,
name: str, name: str,
server_version: Optional[Tuple[Any, ...]] = None, methods: list,
) -> None: ) -> None:
self.account = account self.account = account
self.basepath = basepath self.basepath = basepath
self.client = client self.client = client
self.name = name self.name = name
self.server_version = server_version self.methods = methods
self.schema: Optional[Type] = None self.schema = None
def _step_into_response(self, response: dict, return_type: Union[str, List, None]): def _step_into_response(self, response: dict, return_type: str or List):
"""Step into the dict to get the relevant data""" """Step into the dict to get the relevant data"""
if return_type is None: if return_type is None:
return response return response
if isinstance(return_type, str): elif isinstance(return_type, str):
return response[return_type] return response[return_type]
if isinstance(return_type, List): elif isinstance(return_type, List):
for key in return_type: for key in return_type:
response = response[key] response = response[key]
return response return response
def _parse_response(self, response: Union[dict, list, None], schema=None): def _parse_response(self, response: dict or list, schema=None):
"""Try to create a class instance from the response""" """Try to create a class instance from the response"""
if response is None:
return None
if isinstance(response, list): if isinstance(response, list):
return [self._parse_response(response=r, schema=schema) for r in response] return [self._parse_response(response=r, schema=schema) for r in response]
if schema: if schema:
@@ -58,26 +52,26 @@ class ResourceBase(object):
def make_request( def make_request(
self, self,
query: DocumentNode, query: gql,
params: Dict = None, params: Dict = None,
return_type: Union[str, List, None] = None, return_type: str or List = None,
schema=None, schema=None,
parse_response: bool = True, parse_response: bool = True,
) -> Any: ) -> Dict or GraphQLException:
"""Executes the GraphQL query""" """Executes the GraphQL query"""
try: try:
response = self.client.execute(query, variable_values=params) response = self.client.execute(query, variable_values=params)
except Exception as ex: except Exception as e:
if isinstance(ex, TransportQueryError): if isinstance(e, TransportQueryError):
return GraphQLException( return GraphQLException(
message=f"Failed to execute the GraphQL {self.name} request. Errors: {ex.errors}", message=f"Failed to execute the GraphQL {self.name} request. Errors: {e.errors}",
errors=ex.errors, errors=e.errors,
data=ex.data, data=e.data,
) )
else: else:
return SpeckleException( return SpeckleException(
message=f"Failed to execute the GraphQL {self.name} request. Inner exception: {ex}", message=f"Failed to execute the GraphQL {self.name} request. Inner exception: {e}",
exception=ex, exception=e,
) )
response = self._step_into_response(response=response, return_type=return_type) response = self._step_into_response(response=response, return_type=return_type)
@@ -86,29 +80,3 @@ class ResourceBase(object):
return self._parse_response(response=response, schema=schema) return self._parse_response(response=response, schema=schema)
else: else:
return response return response
def _check_server_version_at_least(
self, target_version: Tuple[Any, ...], unsupported_message: str = None
):
"""Use this check to guard against making unsupported requests on older servers.
Arguments:
target_version {tuple} -- the minimum 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
"""
if not unsupported_message:
unsupported_message = f"The client method used is not supported on Speckle Server versios prior to v{'.'.join(target_version)}"
if self.server_version and self.server_version < target_version:
raise UnsupportedException(unsupported_message)
def _check_invites_supported(self):
"""Invites are only supported for Speckle Server >= 2.6.4.
Use this check to guard against making unsupported requests on older servers.
"""
self._check_server_version_at_least(
(2, 6, 4),
(
"Stream invites are only supported as of Speckle Server v2.6.4. "
"Please update your Speckle Server to use this method or use the `grant_permission` flow instead."
),
)
+1
View File
@@ -1,5 +1,6 @@
from pathlib import Path from pathlib import Path
import sys import sys
import inspect
import pkgutil import pkgutil
from importlib import import_module from importlib import import_module
+2
View File
@@ -4,6 +4,7 @@ from specklepy.api.models import Branch
from specklepy.logging import metrics from specklepy.logging import metrics
NAME = "branch" NAME = "branch"
METHODS = ["create"]
class Resource(ResourceBase): class Resource(ResourceBase):
@@ -15,6 +16,7 @@ class Resource(ResourceBase):
basepath=basepath, basepath=basepath,
client=client, client=client,
name=NAME, name=NAME,
methods=METHODS,
) )
self.schema = Branch self.schema = Branch
+2
View File
@@ -6,6 +6,7 @@ from specklepy.logging import metrics
NAME = "commit" NAME = "commit"
METHODS = []
class Resource(ResourceBase): class Resource(ResourceBase):
@@ -17,6 +18,7 @@ class Resource(ResourceBase):
basepath=basepath, basepath=basepath,
client=client, client=client,
name=NAME, name=NAME,
methods=METHODS,
) )
self.schema = Commit self.schema = Commit
+2
View File
@@ -4,6 +4,7 @@ from specklepy.api.resource import ResourceBase
from specklepy.objects.base import Base from specklepy.objects.base import Base
NAME = "object" NAME = "object"
METHODS = []
class Resource(ResourceBase): class Resource(ResourceBase):
@@ -15,6 +16,7 @@ class Resource(ResourceBase):
basepath=basepath, basepath=basepath,
client=client, client=client,
name=NAME, name=NAME,
methods=METHODS,
) )
self.schema = Base self.schema = Base
+3 -36
View File
@@ -1,13 +1,12 @@
import re from typing import Dict, List
from typing import Any, Dict, List, Tuple
from gql import gql from gql import gql
from specklepy.api.models import ServerInfo from specklepy.api.models import ServerInfo
from specklepy.api.resource import ResourceBase from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics from specklepy.logging import metrics
from specklepy.logging.exceptions import GraphQLException
NAME = "server" NAME = "server"
METHODS = ["get", "apps"]
class Resource(ResourceBase): class Resource(ResourceBase):
@@ -19,6 +18,7 @@ class Resource(ResourceBase):
basepath=basepath, basepath=basepath,
client=client, client=client,
name=NAME, name=NAME,
methods=METHODS,
) )
def get(self) -> ServerInfo: def get(self) -> ServerInfo:
@@ -61,39 +61,6 @@ class Resource(ResourceBase):
query=query, return_type="serverInfo", schema=ServerInfo query=query, return_type="serverInfo", schema=ServerInfo
) )
def version(self) -> Tuple[Any, ...]:
"""Get the server version
Returns:
tuple -- 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: def apps(self) -> Dict:
"""Get the apps registered on the server """Get the apps registered on the server
+6 -300
View File
@@ -1,26 +1,26 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import List, Optional
from deprecated import deprecated
from gql import gql from gql import gql
from typing import List
from specklepy.logging import metrics from specklepy.logging import metrics
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, Stream from specklepy.api.models import ActivityCollection, Stream
from specklepy.api.resource import ResourceBase from specklepy.api.resource import ResourceBase
from specklepy.logging.exceptions import UnsupportedException, SpeckleException from specklepy.logging.exceptions import SpeckleException
NAME = "stream" NAME = "stream"
METHODS = ["list", "create", "get", "update", "delete", "search", "activity"]
class Resource(ResourceBase): class Resource(ResourceBase):
"""API Access class for streams""" """API Access class for streams"""
def __init__(self, account, basepath, client, server_version) -> None: def __init__(self, account, basepath, client) -> None:
super().__init__( super().__init__(
account=account, account=account,
basepath=basepath, basepath=basepath,
client=client, client=client,
name=NAME, name=NAME,
server_version=server_version, methods=METHODS,
) )
self.schema = Stream self.schema = Stream
@@ -342,18 +342,9 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["streamFavorite"] query=query, params=params, return_type=["streamFavorite"]
) )
@deprecated(
version="2.6.4",
reason=(
"As of Speckle Server v2.6.4, this method is deprecated. "
"Users need to be invited and accept the invite before being added to a stream"
),
)
def grant_permission(self, stream_id: str, user_id: str, role: str): def grant_permission(self, stream_id: str, user_id: str, role: str):
"""Grant permissions to a user on a given stream """Grant permissions to a user on a given stream
Valid for Speckle Server version < 2.6.4
Arguments: Arguments:
stream_id {str} -- the id of the stream to grant permissions to stream_id {str} -- the id of the stream to grant permissions to
user_id {str} -- the id of the user to grant permissions for user_id {str} -- the id of the user to grant permissions for
@@ -363,15 +354,6 @@ class Resource(ResourceBase):
bool -- True if the operation was successful 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})
if self.server_version and self.server_version >= (2, 6, 4):
raise UnsupportedException(
(
"Server mutation `grant_permission` is no longer supported as of Speckle Server v2.6.4. "
"Please use the new `update_permission` method to change an existing user's permission "
"or use the `invite` method to invite a user to a stream."
)
)
query = gql( query = gql(
""" """
mutation StreamGrantPermission($permission_params: StreamGrantPermissionInput !) { mutation StreamGrantPermission($permission_params: StreamGrantPermissionInput !) {
@@ -395,282 +377,6 @@ class Resource(ResourceBase):
parse_response=False, parse_response=False,
) )
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
"""
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,
)
def invite(
self,
stream_id: str,
email: str = None,
user_id: str = None,
role: str = "stream:contributor", # should default be reviewer?
message: 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 assing 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
"""
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,
)
def invite_batch(
self,
stream_id: str,
emails: List[str] = None,
user_ids: List[None] = None,
message: 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
"""
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
]
user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id}
for user_id in user_ids
if user_ids 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
"""
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,
)
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
"""
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,
)
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
"""
metrics.track(
metrics.PERMISSION, self.account, {"name": "update", "role": role}
)
if self.server_version 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): def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream """Revoke permissions from a user on a given stream
+10 -5
View File
@@ -1,12 +1,16 @@
from typing import Callable, Dict, List, Union from typing import Callable, Dict, List
from functools import wraps from functools import wraps
from gql import gql from gql import gql
from graphql import DocumentNode
from specklepy.api.resource import ResourceBase from specklepy.api.resource import ResourceBase
from specklepy.api.resources.stream import Stream from specklepy.api.resources.stream import Stream
from specklepy.logging.exceptions import SpeckleException from specklepy.logging.exceptions import SpeckleException
NAME = "subscribe" NAME = "subscribe"
METHODS = [
"stream_added",
"stream_updated",
"stream_removed",
]
def check_wsclient(function): def check_wsclient(function):
@@ -31,6 +35,7 @@ class Resource(ResourceBase):
basepath=basepath, basepath=basepath,
client=client, client=client,
name=NAME, name=NAME,
methods=METHODS,
) )
@check_wsclient @check_wsclient
@@ -104,15 +109,15 @@ class Resource(ResourceBase):
@check_wsclient @check_wsclient
async def subscribe( async def subscribe(
self, self,
query: DocumentNode, query: gql,
params: Dict = None, params: Dict = None,
callback: Callable = None, callback: Callable = None,
return_type: Union[str, List] = None, return_type: str or List = None,
schema=None, schema=None,
parse_response: bool = True, parse_response: bool = True,
): ):
# if self.client.transport.websocket is None: # if self.client.transport.websocket is None:
# TODO: add multiple subs to the same ws connection # TODO: add multiple subs to the same ws connection
async with self.client as session: async with self.client as session:
async for res in session.subscribe(query, variable_values=params): async for res in session.subscribe(query, variable_values=params):
res = self._step_into_response(response=res, return_type=return_type) res = self._step_into_response(response=res, return_type=return_type)
+7 -97
View File
@@ -1,24 +1,25 @@
from typing import List, Optional, Union
from datetime import datetime, timezone from datetime import datetime, timezone
from gql import gql
from specklepy.logging import metrics from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException from specklepy.logging.exceptions import SpeckleException
from typing import List
from gql import gql
from specklepy.api.resource import ResourceBase from specklepy.api.resource import ResourceBase
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User from specklepy.api.models import ActivityCollection, User
NAME = "user" NAME = "user"
METHODS = ["get", "search", "update", "activity"]
class Resource(ResourceBase): class Resource(ResourceBase):
"""API Access class for users""" """API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None: def __init__(self, account, basepath, client) -> None:
super().__init__( super().__init__(
account=account, account=account,
basepath=basepath, basepath=basepath,
client=client, client=client,
name=NAME, name=NAME,
server_version=server_version, methods=METHODS,
) )
self.schema = User self.schema = User
@@ -54,9 +55,7 @@ class Resource(ResourceBase):
return self.make_request(query=query, params=params, return_type="user") return self.make_request(query=query, params=params, return_type="user")
def search( def search(self, search_query: str, limit: int = 25) -> List[User]:
self, search_query: str, limit: int = 25
) -> Union[List[User], SpeckleException]:
"""Searches for user by name or email. The search query must be at least 3 characters long """Searches for user by name or email. The search query must be at least 3 characters long
Arguments: Arguments:
@@ -189,92 +188,3 @@ class Resource(ResourceBase):
return_type=["user", "activity"], return_type=["user", "activity"],
schema=ActivityCollection, 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
"""
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,
)
def get_pending_invite(
self, stream_id: str, token: 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)
"""
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,
)
+6 -17
View File
@@ -1,9 +1,8 @@
from typing import Any, List, Optional from typing import Any, List
class SpeckleException(Exception): class SpeckleException(Exception):
def __init__(self, message: str, exception: Exception = None) -> None: def __init__(self, message: str, exception: Exception = None) -> None:
super().__init__()
self.message = message self.message = message
self.exception = exception self.exception = exception
@@ -12,19 +11,17 @@ class SpeckleException(Exception):
class SerializationException(SpeckleException): class SerializationException(SpeckleException):
def __init__(self, message: str, obj: Any, exception: Exception = None) -> None: def __init__(self, message: str, object: Any, exception: Exception = None) -> None:
super().__init__(message=message, exception=exception) super().__init__(message=message)
self.obj = obj self.object = object
self.unhandled_type = type(obj) self.unhandled_type = type(object)
def __str__(self) -> str: def __str__(self) -> str:
return f"SpeckleException: Could not serialize object of type {self.unhandled_type}" return f"SpeckleException: Could not serialize object of type {self.unhandled_type}"
class GraphQLException(SpeckleException): class GraphQLException(SpeckleException):
def __init__( def __init__(self, message: str, errors: List, data=None) -> None:
self, message: str, errors: Optional[List[Any]] = None, data=None
) -> None:
super().__init__(message=message) super().__init__(message=message)
self.errors = errors self.errors = errors
self.data = data self.data = data
@@ -33,14 +30,6 @@ class GraphQLException(SpeckleException):
return f"GraphQLException: {self.message}" return f"GraphQLException: {self.message}"
class UnsupportedException(SpeckleException):
def __init__(self, message: str) -> None:
super().__init__(message=message)
def __str__(self) -> str:
return f"UnsupportedException: {self.message}"
class SpeckleWarning(Warning): class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None: def __init__(self, *args: object) -> None:
super().__init__(*args) super().__init__(*args)
+8 -14
View File
@@ -1,13 +1,10 @@
import socket
import sys import sys
import queue import queue
import hashlib import hashlib
import getpass
import logging import logging
import requests import requests
import threading import threading
import platform
import contextlib
""" """
Anonymous telemetry to help us understand how to make a better Speckle. Anonymous telemetry to help us understand how to make a better Speckle.
@@ -15,7 +12,7 @@ This really helps us to deliver a better open source project and product!
""" """
TRACK = True TRACK = True
HOST_APP = "python" HOST_APP = "python"
HOST_APP_VERSION = f"python {'.'.join(map(str, sys.version_info[:2]))}" HOST_APP_VERSION = f"python {'.'.join(map(str, sys.version_info[:3]))}"
PLATFORMS = {"win32": "Windows", "cygwin": "Windows", "darwin": "Mac OS X"} PLATFORMS = {"win32": "Windows", "cygwin": "Windows", "darwin": "Mac OS X"}
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -26,7 +23,6 @@ RECEIVE = "Receive"
SEND = "Send" SEND = "Send"
STREAM = "Stream Action" STREAM = "Stream Action"
PERMISSION = "Permission Action" PERMISSION = "Permission Action"
INVITE = "Invite Action"
COMMIT = "Commit Action" COMMIT = "Commit Action"
BRANCH = "Branch Action" BRANCH = "Branch Action"
USER = "User Action" USER = "User Action"
@@ -79,7 +75,7 @@ def track(action: str, account: "Account" = None, custom_props: dict = None):
METRICS_TRACKER.queue.put_nowait(event_params) METRICS_TRACKER.queue.put_nowait(event_params)
except Exception as ex: except Exception as ex:
# wrapping this whole thing in a try except as we never want a failure here to annoy users! # wrapping this whole thing in a try except as we never want a failure here to annoy users!
LOG.error(f"Error queueing metrics request: {str(ex)}") LOG.error("Error queueing metrics request: " + str(ex))
def initialise_tracker(account: "Account" = None): def initialise_tracker(account: "Account" = None):
@@ -105,7 +101,8 @@ class Singleton(type):
class MetricsTracker(metaclass=Singleton): class MetricsTracker(metaclass=Singleton):
analytics_url = "https://analytics.speckle.systems/track?ip=1" analytics_url = "https://analytics.speckle.systems/track?ip=1"
analytics_token = "acd87c5a50b56df91a795e999812a3a4" analytics_token = "acd87c5a50b56df91a795e999812a3a4"
last_user = "" user_ip = None
last_user = None
last_server = None last_server = None
platform = None platform = None
sending_thread = None sending_thread = None
@@ -117,15 +114,12 @@ class MetricsTracker(metaclass=Singleton):
) )
self.platform = PLATFORMS.get(sys.platform, "linux") self.platform = PLATFORMS.get(sys.platform, "linux")
self.sending_thread.start() self.sending_thread.start()
with contextlib.suppress(Exception): self.user_ip = socket.gethostbyname(socket.gethostname())
node, user = platform.node(), getpass.getuser()
if node and user:
self.last_user = f"@{self.hash(f'{node}-{user}')}"
def set_last_user(self, email: str): def set_last_user(self, email: str):
if not email: if not email:
return return
self.last_user = f"@{self.hash(email)}" self.last_user = "@" + self.hash(email)
def set_last_server(self, server: str): def set_last_server(self, server: str):
if not server: if not server:
@@ -143,6 +137,6 @@ class MetricsTracker(metaclass=Singleton):
try: try:
session.post(self.analytics_url, json=event_params) session.post(self.analytics_url, json=event_params)
except Exception as ex: except Exception as ex:
LOG.error(f"Error sending metrics request: {str(ex)}") LOG.error("Error sending metrics request: " + str(ex))
self.queue.task_done() self.queue.task_done()
+9 -12
View File
@@ -92,19 +92,12 @@ class _RegisteringBase:
speckle_type: ClassVar[str] speckle_type: ClassVar[str]
_type_registry: ClassVar[Dict[str, "Base"]] = {} _type_registry: ClassVar[Dict[str, "Base"]] = {}
_attr_types: ClassVar[Dict[str, Type]] = {} _attr_types: ClassVar[Dict[str, Type]] = {}
# dict of chunkable props and their max chunk size
_chunkable: Dict[str, int] = {}
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
_serialize_ignore: Set[str] = set()
class Config: class Config:
validate_assignment = True validate_assignment = True
@classmethod @classmethod
def get_registered_type( def get_registered_type(cls, speckle_type: str) -> Optional[Type["Base"]]:
cls, speckle_type: str
) -> Union["Base", Type["Base"], None]:
"""Get the registered type from the protected mapping via the `speckle_type`""" """Get the registered type from the protected mapping via the `speckle_type`"""
return cls._type_registry.get(speckle_type, None) return cls._type_registry.get(speckle_type, None)
@@ -149,7 +142,12 @@ class Base(_RegisteringBase):
id: Optional[str] = None id: Optional[str] = None
totalChildrenCount: Optional[int] = None totalChildrenCount: Optional[int] = None
applicationId: Optional[str] = None applicationId: Optional[str] = None
_units: Union[str, None] = None _units: str = "m"
# dict of chunkable props and their max chunk size
_chunkable: Dict[str, int] = {}
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
_serialize_ignore: Set[str] = set()
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__() super().__init__()
@@ -333,7 +331,7 @@ class Base(_RegisteringBase):
def get_serializable_attributes(self) -> List[str]: def get_serializable_attributes(self) -> List[str]:
"""Get the attributes that should be serialized""" """Get the attributes that should be serialized"""
return sorted(list(set(self.get_member_names()) - self._serialize_ignore)) return list(set(self.get_member_names()) - self._serialize_ignore)
def get_typed_member_names(self) -> List[str]: def get_typed_member_names(self) -> List[str]:
"""Get all of the names of the defined (typed) properties of this object""" """Get all of the names of the defined (typed) properties of this object"""
@@ -380,7 +378,6 @@ class Base(_RegisteringBase):
) )
def _handle_object_count(self, obj: Any, parsed: List) -> int: def _handle_object_count(self, obj: Any, parsed: List) -> int:
# pylint: disable=isinstance-second-argument-not-valid-type
count = 0 count = 0
if obj is None: if obj is None:
return count return count
@@ -409,7 +406,7 @@ Base.update_forward_refs()
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"): class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: Union[List[Any], None] = None data: List[Any] = None
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
+18 -13
View File
@@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import Any, Callable, List, Type, Dict from typing import Any, Callable, List, Type
from specklepy.logging.exceptions import SpeckleException from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base from specklepy.objects.base import Base
@@ -43,8 +43,8 @@ def curve_from_list(args: List[float]):
class ObjectArray: class ObjectArray:
def __init__(self, data: list = None) -> None: def __init__(self) -> None:
self.data = data or [] self.data = []
@classmethod @classmethod
def from_objects(cls, objects: List[Base]) -> "ObjectArray": def from_objects(cls, objects: List[Base]) -> "ObjectArray":
@@ -60,17 +60,18 @@ class ObjectArray:
"All objects in chunk should have the same speckle_type. " "All objects in chunk should have the same speckle_type. "
f"Found {speckle_type} and {obj.speckle_type}" f"Found {speckle_type} and {obj.speckle_type}"
) )
data_list.encode_object(obj=obj) data_list.encode_object(object=obj)
return data_list return data_list
@staticmethod @staticmethod
def decode_data( def decode_data(
data: List[Any], decoder: Callable[[List[Any]], Base], **kwargs: Dict[str, Any] data: List[Any], decoder: Callable[[List[Any]], Base]
) -> List[Base]: ) -> List[Base]:
bases = [] bases = []
if not data: if not data:
return bases return bases
index = 0 index = 0
while index < len(data): while index < len(data):
item_length = int(data[index]) item_length = int(data[index])
@@ -78,16 +79,19 @@ class ObjectArray:
item_end = item_start + item_length item_end = item_start + item_length
item_data = data[item_start:item_end] item_data = data[item_start:item_end]
index = item_end index = item_end
decoded_data = decoder(item_data, **kwargs) # TODO: investigate what's going on w this fail
bases.append(decoded_data) try:
decoded_data = decoder(item_data)
bases.append(decoded_data)
except ValueError:
continue
return bases return bases
def decode(self, decoder: Callable[[List[Any]], Any], **kwargs: Dict[str, Any]): def decode(self, decoder: Callable[[List[Any]], Any]):
return self.decode_data(data=self.data, decoder=decoder, **kwargs) return self.decode_data(data=self.data, decoder=decoder)
def encode_object(self, obj: Base): def encode_object(self, object: Base):
encoded = obj.to_list() encoded = object.to_list()
encoded.insert(0, len(encoded)) encoded.insert(0, len(encoded))
self.data.extend(encoded) self.data.extend(encoded)
@@ -124,7 +128,8 @@ class CurveArray(ObjectArray):
@classmethod @classmethod
def _curve_decoder(cls, data: List[float]) -> Base: def _curve_decoder(cls, data: List[float]) -> Base:
crv_array = cls(data) crv_array = cls()
crv_array.data = data
return crv_array.to_curve() return crv_array.to_curve()
def to_curves(self) -> List[Base]: def to_curves(self) -> List[Base]:
+166 -284
View File
@@ -64,21 +64,19 @@ class Plane(Base, speckle_type=GEOMETRY + "Plane"):
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Plane": def from_list(cls, args: List[Any]) -> "Plane":
return cls( return cls(
origin=Point.from_list(args[:3]), origin=Point.from_list(args[0:3]),
normal=Vector.from_list(args[3:6]), normal=Vector.from_list(args[3:6]),
xdir=Vector.from_list(args[6:9]), xdir=Vector.from_list(args[6:9]),
ydir=Vector.from_list(args[9:12]), ydir=Vector.from_list(args[9:12]),
units=get_units_from_encoding(args[-1]),
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
return [ encoded = []
*self.origin.to_list(), encoded.extend(self.origin.to_list())
*self.normal.to_list(), encoded.extend(self.normal.to_list())
*self.xdir.to_list(), encoded.extend(self.xdir.to_list())
*self.ydir.to_list(), encoded.extend(self.ydir.to_list())
get_encoding_from_units(self.units), return encoded
]
class Box(Base, speckle_type=GEOMETRY + "Box"): class Box(Base, speckle_type=GEOMETRY + "Box"):
@@ -100,21 +98,17 @@ class Line(Base, speckle_type=GEOMETRY + "Line"):
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Line": def from_list(cls, args: List[Any]) -> "Line":
return cls( return cls(
start=Point.from_list(args[1:4]), start=Point.from_list(args[0:3]),
end=Point.from_list(args[4:7]), end=Point.from_list(args[3:6]),
domain=Interval.from_list(args[7:10]), domain=Interval.from_list(args[6:9]),
units=get_units_from_encoding(args[-1]),
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
domain = self.domain.to_list() if self.domain else [0, 1] encoded = []
return [ encoded.extend(self.start.to_list())
CurveTypeEncoding.Line.value, encoded.extend(self.end.to_list())
*self.start.to_list(), encoded.extend(self.domain.to_list())
*self.end.to_list(), return encoded
*domain,
get_encoding_from_units(self.units),
]
class Arc(Base, speckle_type=GEOMETRY + "Arc"): class Arc(Base, speckle_type=GEOMETRY + "Arc"):
@@ -140,26 +134,20 @@ class Arc(Base, speckle_type=GEOMETRY + "Arc"):
angleRadians=args[4], angleRadians=args[4],
domain=Interval.from_list(args[5:7]), domain=Interval.from_list(args[5:7]),
plane=Plane.from_list(args[7:20]), plane=Plane.from_list(args[7:20]),
startPoint=Point.from_list(args[20:23]),
midPoint=Point.from_list(args[23:26]),
endPoint=Point.from_list(args[26:29]),
units=get_units_from_encoding(args[-1]), units=get_units_from_encoding(args[-1]),
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
return [ encoded = []
CurveTypeEncoding.Arc.value, encoded.append(CurveTypeEncoding.Arc.value)
self.radius, encoded.append(self.radius)
self.startAngle, encoded.append(self.startAngle)
self.endAngle, encoded.append(self.endAngle)
self.angleRadians, encoded.append(self.angleRadians)
*self.domain.to_list(), encoded.extend(self.domain.to_list())
*self.plane.to_list(), encoded.extend(self.plane.to_list())
*self.startPoint.to_list(), encoded.append(get_encoding_from_units(self.units))
*self.midPoint.to_list(), return encoded
*self.endPoint.to_list(),
get_encoding_from_units(self.units),
]
class Circle(Base, speckle_type=GEOMETRY + "Circle"): class Circle(Base, speckle_type=GEOMETRY + "Circle"):
@@ -180,13 +168,13 @@ class Circle(Base, speckle_type=GEOMETRY + "Circle"):
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
return [ encoded = []
CurveTypeEncoding.Circle.value, encoded.append(CurveTypeEncoding.Circle.value)
self.radius, encoded.append(self.radius),
*self.domain.to_list(), encoded.extend(self.domain.to_list())
*self.plane.to_list(), encoded.extend(self.plane.to_list())
get_encoding_from_units(self.units), encoded.append(get_encoding_from_units(self.units))
] return encoded
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"): class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
@@ -210,14 +198,14 @@ class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
return [ encoded = []
CurveTypeEncoding.Ellipse.value, encoded.append(CurveTypeEncoding.Ellipse.value)
self.firstRadius, encoded.append(self.firstRadius)
self.secondRadius, encoded.append(self.secondRadius)
*self.domain.to_list(), encoded.extend(self.domain.to_list())
*self.plane.to_list(), encoded.extend(self.plane.to_list())
get_encoding_from_units(self.units), encoded.append(get_encoding_from_units(self.units))
] return encoded
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}): class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
@@ -249,14 +237,14 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
return [ encoded = []
CurveTypeEncoding.Polyline.value, encoded.append(CurveTypeEncoding.Polyline.value)
int(self.closed), encoded.append(int(self.closed))
*self.domain.to_list(), encoded.extend(self.domain.to_list())
len(self.value), encoded.append(len(self.value))
*self.value, encoded.extend(self.value)
get_encoding_from_units(self.units), encoded.append(get_encoding_from_units(self.units))
] return encoded
def as_points(self) -> List[Point]: def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points""" """Converts the `value` attribute to a list of Points"""
@@ -327,21 +315,21 @@ class Curve(
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
return [ encoded = []
CurveTypeEncoding.Curve.value, encoded.append(CurveTypeEncoding.Curve.value)
self.degree, encoded.append(self.degree)
int(self.periodic), encoded.append(int(self.periodic))
int(self.rational), encoded.append(int(self.rational))
int(self.closed), encoded.append(int(self.closed))
*self.domain.to_list(), encoded.extend(self.domain.to_list())
len(self.points), encoded.append(len(self.points))
len(self.weights), encoded.append(len(self.weights))
len(self.knots), encoded.append(len(self.knots))
*self.points, encoded.extend(self.points)
*self.weights, encoded.extend(self.weights)
*self.knots, encoded.extend(self.knots)
get_encoding_from_units(self.units), encoded.append(get_encoding_from_units(self.units))
] return encoded
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"): class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
@@ -354,7 +342,8 @@ class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Polycurve": def from_list(cls, args: List[Any]) -> "Polycurve":
curve_arrays = CurveArray(args[5:-1]) curve_arrays = CurveArray()
curve_arrays.data = args[4:-1]
return cls( return cls(
closed=bool(args[1]), closed=bool(args[1]),
domain=Interval.from_list(args[2:4]), domain=Interval.from_list(args[2:4]),
@@ -363,15 +352,14 @@ class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
curve_array = CurveArray.from_curves(self.segments).data encoded = []
return [ encoded.append(CurveTypeEncoding.Polycurve.value)
CurveTypeEncoding.Polycurve.value, encoded.append(int(self.closed))
int(self.closed), encoded.extend(self.domain.to_list())
*self.domain.to_list(), curve_array = CurveArray.from_curves(self.segments)
len(curve_array), encoded.extend(curve_array.data)
*curve_array, encoded.append(get_encoding_from_units(self.units))
get_encoding_from_units(self.units), return encoded
]
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"): class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
@@ -472,65 +460,46 @@ class Surface(Base, speckle_type=GEOMETRY + "Surface"):
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
return [ encoded = []
self.degreeU, encoded.append(self.degreeU)
self.degreeV, encoded.append(self.degreeV)
self.countU, encoded.append(self.countU)
self.countV, encoded.append(self.countV)
int(self.rational), encoded.append(int(self.rational))
int(self.closedU), encoded.append(int(self.closedU))
int(self.closedV), encoded.append(int(self.closedV))
*self.domainU.to_list(), encoded.extend(self.domainU.to_list())
*self.domainV.to_list(), encoded.extend(self.domainV.to_list())
len(self.pointData), encoded.append(len(self.pointData))
len(self.knotsU), encoded.append(len(self.knotsU))
len(self.knotsV), encoded.append(len(self.knotsV))
*self.pointData, encoded.extend(self.pointData)
*self.knotsU, encoded.extend(self.knotsU)
*self.knotsV, encoded.extend(self.knotsV)
get_encoding_from_units(self.units), encoded.append(get_encoding_from_units(self.units))
] return encoded
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"): class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
_Brep: "Brep" = None _Brep: "Brep" = None
SurfaceIndex: int = None SurfaceIndex: int = None
LoopIndices: List[int] = None
OuterLoopIndex: int = None OuterLoopIndex: int = None
OrientationReversed: bool = None OrientationReversed: bool = None
LoopIndices: List[int] = None
@property @property
def _outer_loop(self): def _outer_loop(self):
return self._Brep.Loops[self.OuterLoopIndex] # pylint: disable=no-member return self._Brep.Loops[self.OuterLoopIndex]
@property @property
def _surface(self): def _surface(self):
return self._Brep.Surfaces[self.SurfaceIndex] # pylint: disable=no-member return self._Brep.Surfaces[self.SurfaceIndex]
@property @property
def _loops(self): def _loops(self):
if self.LoopIndices: if self.LoopIndices:
# pylint: disable=not-an-iterable, no-member
return [self._Brep.Loops[i] for i in self.LoopIndices] return [self._Brep.Loops[i] for i in self.LoopIndices]
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepFace":
return cls(
_Brep=brep,
SurfaceIndex=args[0],
OuterLoopIndex=args[1],
OrientationReversed=bool(args[2]),
LoopIndices=args[3:],
)
def to_list(self) -> List[Any]:
return [
self.SurfaceIndex,
self.OuterLoopIndex,
int(self.OrientationReversed),
*self.LoopIndices,
]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"): class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
_Brep: "Brep" = None _Brep: "Brep" = None
@@ -552,59 +521,18 @@ class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
@property @property
def _trims(self): def _trims(self):
if self.TrimIndices: if self.TrimIndices:
# pylint: disable=not-an-iterable
return [self._Brep.Trims[i] for i in self.TrimIndices] return [self._Brep.Trims[i] for i in self.TrimIndices]
@property @property
def _curve(self): def _curve(self):
return self._Brep.Curve3D[self.Curve3dIndex] return self._Brep.Curve3D[self.Curve3dIndex]
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepEdge":
domain_start = args[4]
domain_end = args[5]
domain = (
Interval(start=domain_start, end=domain_end)
if None not in (domain_start, domain_end)
else None
)
return cls(
_Brep=brep,
Curve3dIndex=int(args[0]),
TrimIndices=[int(t) for t in args[6:]],
StartIndex=int(args[1]),
EndIndex=int(args[2]),
ProxyCurveIsReversed=bool(args[3]),
Domain=domain,
)
def to_list(self) -> List[Any]:
return [
self.Curve3dIndex,
self.StartIndex,
self.EndIndex,
int(self.ProxyCurveIsReversed),
self.Domain.start,
self.Domain.end,
*self.TrimIndices,
]
class BrepLoopType(int, Enum):
Unknown = 0
Outer = 1
Inner = 2
Slit = 3
CurveOnSurface = 4
PointOnSurface = 5
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"): class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
_Brep: "Brep" = None _Brep: "Brep" = None
FaceIndex: int = None FaceIndex: int = None
TrimIndices: List[int] = None TrimIndices: List[int] = None
Type: BrepLoopType = None Type: str = None
@property @property
def _face(self): def _face(self):
@@ -613,27 +541,10 @@ class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
@property @property
def _trims(self): def _trims(self):
if self.TrimIndices: if self.TrimIndices:
# pylint: disable=not-an-iterable
return [self._Brep.Trims[i] for i in self.TrimIndices] return [self._Brep.Trims[i] for i in self.TrimIndices]
@classmethod
def from_list(cls, args: List[any], brep: "Brep" = None):
return cls(
_Brep=brep,
FaceIndex=args[0],
Type=BrepLoopType(args[1]),
TrimIndices=args[2:],
)
def to_list(self) -> List[int]: class BrepTrimTypeEnum(int, Enum):
return [
self.FaceIndex,
self.Type.value,
*self.TrimIndices,
]
class BrepTrimType(int, Enum):
Unknown = 0 Unknown = 0
Boundary = 1 Boundary = 1
Mated = 2 Mated = 2
@@ -653,35 +564,29 @@ class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
LoopIndex: int = None LoopIndex: int = None
CurveIndex: int = None CurveIndex: int = None
IsoStatus: int = None IsoStatus: int = None
TrimType: BrepTrimType = None TrimType: str = None
IsReversed: bool = None IsReversed: bool = None
Domain: Interval = None Domain: Interval = None
@property @property
def _face(self): def _face(self):
if self._Brep: return self._Brep.Faces[self.FaceIndex]
return self._Brep.Faces[self.FaceIndex] # pylint: disable=no-member
@property @property
def _loop(self): def _loop(self):
if self._Brep: return self._Brep.Loops[self.LoopIndex]
return self._Brep.Loops[self.LoopIndex] # pylint: disable=no-member
@property @property
def _edge(self): def _edge(self):
if self._Brep: return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
# pylint: disable=no-member
return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
@property @property
def _curve_2d(self): def _curve_2d(self):
if self._Brep: return self._Brep.Curve2D[self.CurveIndex]
return self._Brep.Curve2D[self.CurveIndex] # pylint: disable=no-member
@classmethod @classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepTrim": def from_list(cls, args: List[Any]) -> "BrepTrim":
return cls( return cls(
_Brep=brep,
EdgeIndex=args[0], EdgeIndex=args[0],
StartIndex=args[1], StartIndex=args[1],
EndIndex=args[2], EndIndex=args[2],
@@ -689,48 +594,39 @@ class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
LoopIndex=args[4], LoopIndex=args[4],
CurveIndex=args[5], CurveIndex=args[5],
IsoStatus=args[6], IsoStatus=args[6],
TrimType=BrepTrimType(args[7]), TrimType=BrepTrimTypeEnum(args[7]).name,
IsReversed=bool(args[8]), IsReversed=bool(args[8]),
) )
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
return [ encoded = []
self.EdgeIndex, encoded.append(self.EdgeIndex)
self.StartIndex, encoded.append(self.StartIndex)
self.EndIndex, encoded.append(self.EndIndex)
self.FaceIndex, encoded.append(self.FaceIndex)
self.LoopIndex, encoded.append(self.LoopIndex)
self.CurveIndex, encoded.append(self.CurveIndex)
self.IsoStatus, encoded.append(self.IsoStatus)
self.TrimType.value, encoded.append(getattr(BrepTrimTypeEnum, self.TrimType).value)
int(self.IsReversed), encoded.append(self.IsReversed)
] return encoded
class Brep( class Brep(
Base, Base,
speckle_type=GEOMETRY + "Brep", speckle_type=GEOMETRY + "Brep",
chunkable={ chunkable={
"SurfacesValue": 31250, "SurfacesValue": 200,
"Curve3DValues": 31250, "Curve3DValues": 200,
"Curve2DValues": 31250, "Curve2DValues": 200,
"VerticesValue": 31250, "VerticesValue": 5000,
"EdgesValue": 62500, "Edges": 5000,
"LoopsValue": 62500, "Loops": 5000,
"FacesValue": 62500, "TrimsValue": 5000,
"TrimsValue": 62500, "Faces": 5000,
}, },
detachable={"displayValue"}, detachable={"displayValue"},
serialize_ignore={ serialize_ignore={"Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"},
"Surfaces",
"Curve3D",
"Curve2D",
"Vertices",
"Trims",
"Edges",
"Loops",
"Faces",
},
): ):
provenance: str = None provenance: str = None
bbox: Box = None bbox: Box = None
@@ -741,10 +637,6 @@ class Brep(
Curve3D: List[Base] = None Curve3D: List[Base] = None
Curve2D: List[Base] = None Curve2D: List[Base] = None
Vertices: List[Point] = None Vertices: List[Point] = None
Edges: List[BrepEdge] = None
Loops: List[BrepLoop] = None
Faces: List[BrepFace] = None
Trims: List[BrepTrim] = None
IsClosed: bool = None IsClosed: bool = None
Orientation: int = None Orientation: int = None
@@ -753,7 +645,7 @@ class Brep(
return children return children
for child in children: for child in children:
child._Brep = self # pylint: disable=protected-access child._Brep = self
return children return children
# set as prop for now for backwards compatibility # set as prop for now for backwards compatibility
@@ -769,73 +661,61 @@ class Brep(
self._displayValue = value self._displayValue = value
@property @property
def EdgesValue(self) -> List[BrepEdge]: def Edges(self) -> List[BrepEdge]:
return None if self.Edges is None else ObjectArray.from_objects(self.Edges).data return self._inject_self_into_children(self._Edges)
@EdgesValue.setter @Edges.setter
def EdgesValue(self, value: List[float]): def Edges(self, value: List[BrepEdge]):
if not value: self._Edges = value
return
self.Edges = ObjectArray.decode_data(value, BrepEdge.from_list, brep=self)
@property @property
def LoopsValue(self) -> List[BrepLoop]: def Loops(self) -> List[BrepLoop]:
return None if self.Loops is None else ObjectArray.from_objects(self.Loops).data return self._inject_self_into_children(self._Loops)
@LoopsValue.setter @Loops.setter
def LoopsValue(self, value: List[int]): def Loops(self, value: List[BrepLoop]):
if not value: self._Loops = value
return
self.Loops = ObjectArray.decode_data(value, BrepLoop.from_list, brep=self)
@property @property
def FacesValue(self) -> List[int]: def Faces(self) -> List[BrepFace]:
return None if self.Faces is None else ObjectArray.from_objects(self.Faces).data return self._inject_self_into_children(self._Faces)
@FacesValue.setter @Faces.setter
def FacesValue(self, value: List[int]): def Faces(self, value: List[BrepFace]):
if not value: self._Faces = value
return
self.Faces = ObjectArray.decode_data(value, BrepFace.from_list, brep=self)
@property @property
def SurfacesValue(self) -> List[float]: def SurfacesValue(self) -> List[float]:
return ( if self.Surfaces is None:
None return None
if self.Surfaces is None return ObjectArray.from_objects(self.Surfaces).data
else ObjectArray.from_objects(self.Surfaces).data
)
@SurfacesValue.setter @SurfacesValue.setter
def SurfacesValue(self, value: List[float]): def SurfacesValue(self, value: List[float]):
if not value:
return
self.Surfaces = ObjectArray.decode_data(value, Surface.from_list) self.Surfaces = ObjectArray.decode_data(value, Surface.from_list)
@property @property
def Curve3DValues(self) -> List[float]: def Curve3DValues(self) -> List[float]:
return ( if self.Curve3D is None:
None if self.Curve3D is None else CurveArray.from_curves(self.Curve3D).data return None
) return CurveArray.from_curves(self.Curve3D).data
@Curve3DValues.setter @Curve3DValues.setter
def Curve3DValues(self, value: List[float]): def Curve3DValues(self, value: List[float]):
crv_array = CurveArray(value) crv_array = CurveArray()
crv_array.data = value
self.Curve3D = crv_array.to_curves() self.Curve3D = crv_array.to_curves()
@property @property
def Curve2DValues(self) -> List[Base]: def Curve2DValues(self) -> List[Base]:
return ( if self.Curve2D is None:
None if self.Curve2D is None else CurveArray.from_curves(self.Curve2D).data return None
) return CurveArray.from_curves(self.Curve2D).data
@Curve2DValues.setter @Curve2DValues.setter
def Curve2DValues(self, value: List[float]): def Curve2DValues(self, value: List[float]):
crv_array = CurveArray(value) crv_array = CurveArray()
crv_array.data = value
self.Curve2D = crv_array.to_curves() self.Curve2D = crv_array.to_curves()
@property @property
@@ -862,25 +742,27 @@ class Brep(
self.Vertices = vertices self.Vertices = vertices
# TODO: can this be consistent with loops, edges, faces, curves, etc and prepend with the chunk list? needs to happen in sharp first @property
def Trims(self) -> List[BrepTrim]:
return self._inject_self_into_children(self._Trims)
@Trims.setter
def Trims(self, value: List[BrepTrim]):
self._Trims = value
@property @property
def TrimsValue(self) -> List[float]: def TrimsValue(self) -> List[float]:
# return None if self.Trims is None else ObjectArray.from_objects(self.Trims).data if self.Trims is None:
if not self.Trims: return None
return values = []
value = []
for trim in self.Trims: for trim in self.Trims:
value.extend(trim.to_list()) values.extend(trim.to_list())
return value return values
@TrimsValue.setter @TrimsValue.setter
def TrimsValue(self, value: List[float]): def TrimsValue(self, value: List[float]):
if not value:
return
# self.Trims = ObjectArray.decode_data(value, BrepTrim.from_list, brep=self)
self.Trims = [ self.Trims = [
BrepTrim.from_list(value[i : i + 9], self) for i in range(0, len(value), 9) BrepTrim.from_list(value[i : i + 9]) for i in range(0, len(value), 9)
] ]
+4 -3
View File
@@ -17,7 +17,6 @@ UNITS_STRINGS = {
UNITS_ENCODINGS = { UNITS_ENCODINGS = {
"none": 0, "none": 0,
None: 0,
"mm": 1, "mm": 1,
"cm": 2, "cm": 2,
"m": 3, "m": 3,
@@ -59,5 +58,7 @@ def get_units_from_encoding(unit: int):
def get_encoding_from_units(unit: str): def get_encoding_from_units(unit: str):
try: try:
return UNITS_ENCODINGS[unit] return UNITS_ENCODINGS[unit]
except KeyError as e: except KeyError:
raise SpeckleException(message=f"No encoding exists for unit {unit}. Please enter a valid unit to encode (eg {UNITS_ENCODINGS}).") from e raise SpeckleException(
message=f"No encoding exists for unit {unit}. Please enter a valid unit to encode (eg {UNITS_ENCODINGS})."
)
@@ -40,20 +40,16 @@ def safe_json_loads(obj: str, obj_id=None) -> Any:
class BaseObjectSerializer: class BaseObjectSerializer:
read_transport: AbstractTransport read_transport: AbstractTransport
write_transports: List[AbstractTransport] write_transports: List[AbstractTransport]
detach_lineage: List[bool] # tracks depth and whether or not to detach detach_lineage: List[bool] = [] # tracks depth and whether or not to detach
lineage: List[str] # keeps track of hash chain through the object tree lineage: List[str] = [] # keeps track of hash chain through the object tree
family_tree: Dict[str, Dict[str, int]] family_tree: Dict[str, Dict[str, int]] = {}
closure_table: Dict[str, Dict[str, int]] closure_table: Dict[str, Dict[str, int]] = {}
deserialized: Dict[str, Base] # holds deserialized objects so objects with same id return the same instance
def __init__(self, write_transports: List[AbstractTransport] = None, read_transport=None) -> None: def __init__(
self.write_transports = write_transports or [] self, write_transports: List[AbstractTransport] = [], read_transport=None
) -> None:
self.write_transports = write_transports
self.read_transport = read_transport self.read_transport = read_transport
self.detach_lineage = []
self.lineage = []
self.family_tree = {}
self.closure_table = {}
self.deserialized = {}
def write_json(self, base: Base): def write_json(self, base: Base):
"""Serializes a given base object into a json string """Serializes a given base object into a json string
@@ -61,12 +57,12 @@ class BaseObjectSerializer:
base {Base} -- the base object to be decomposed and serialized base {Base} -- the base object to be decomposed and serialized
Returns: Returns:
(str, str) -- a tuple containing the object id of the base object and the serialized object string (str, str) -- a tuple containing the hash (id) of the base object and the serialized object string
""" """
obj_id, obj = self.traverse_base(base) hash, obj = self.traverse_base(base)
return obj_id, ujson.dumps(obj) return hash, ujson.dumps(obj)
def traverse_base(self, base: Base) -> Tuple[str, Dict]: def traverse_base(self, base: Base) -> Tuple[str, Dict]:
"""Decomposes the given base object and builds a serializable dictionary """Decomposes the given base object and builds a serializable dictionary
@@ -75,7 +71,7 @@ class BaseObjectSerializer:
base {Base} -- the base object to be decomposed and serialized base {Base} -- the base object to be decomposed and serialized
Returns: Returns:
(str, dict) -- a tuple containing the object id of the base object and the constructed serializable dictionary (str, dict) -- a tuple containing the hash (id) of the base object and the constructed serializable dictionary
""" """
self.__reset_writer() self.__reset_writer()
@@ -83,13 +79,13 @@ class BaseObjectSerializer:
for wt in self.write_transports: for wt in self.write_transports:
wt.begin_write() wt.begin_write()
obj_id, obj = self._traverse_base(base) hash, obj = self._traverse_base(base)
if self.write_transports: if self.write_transports:
for wt in self.write_transports: for wt in self.write_transports:
wt.end_write() wt.end_write()
return obj_id, obj return hash, obj
def _traverse_base(self, base: Base) -> Tuple[str, Dict]: def _traverse_base(self, base: Base) -> Tuple[str, Dict]:
if not self.detach_lineage: if not self.detach_lineage:
@@ -114,6 +110,11 @@ class BaseObjectSerializer:
if prop == "id": if prop == "id":
continue continue
# allow serialisation of nulls
if value is None:
object_builder[prop] = value
continue
# only bother with chunking and detaching if there is a write transport # only bother with chunking and detaching if there is a write transport
if self.write_transports: if self.write_transports:
dynamic_chunk_match = prop.startswith("@") and re.match( dynamic_chunk_match = prop.startswith("@") and re.match(
@@ -130,8 +131,8 @@ class BaseObjectSerializer:
prop.startswith("@") or prop in base._detachable or chunkable prop.startswith("@") or prop in base._detachable or chunkable
) )
# 1. handle None and primitives (ints, floats, strings, and bools) # 1. handle primitives (ints, floats, strings, and bools)
if value is None or isinstance(value, PRIMITIVES): if isinstance(value, PRIMITIVES):
object_builder[prop] = value object_builder[prop] = value
continue continue
@@ -144,8 +145,8 @@ class BaseObjectSerializer:
elif isinstance(value, Base): elif isinstance(value, Base):
child_obj = self.traverse_value(value, detach=detach) child_obj = self.traverse_value(value, detach=detach)
if detach and self.write_transports: if detach and self.write_transports:
ref_id = child_obj["id"] ref_hash = child_obj["id"]
object_builder[prop] = self.detach_helper(ref_id=ref_id) object_builder[prop] = self.detach_helper(ref_hash=ref_hash)
else: else:
object_builder[prop] = child_obj object_builder[prop] = child_obj
@@ -164,8 +165,8 @@ class BaseObjectSerializer:
chunk_refs = [] chunk_refs = []
for c in chunks: for c in chunks:
self.detach_lineage.append(detach) self.detach_lineage.append(detach)
ref_id, _ = self._traverse_base(c) ref_hash, _ = self._traverse_base(c)
ref_obj = self.detach_helper(ref_id=ref_id) ref_obj = self.detach_helper(ref_hash=ref_hash)
chunk_refs.append(ref_obj) chunk_refs.append(ref_obj)
object_builder[prop] = chunk_refs object_builder[prop] = chunk_refs
@@ -184,20 +185,20 @@ class BaseObjectSerializer:
} }
object_builder["totalChildrenCount"] = len(closure) object_builder["totalChildrenCount"] = len(closure)
obj_id = hash_obj(object_builder) hash = hash_obj(object_builder)
object_builder["id"] = obj_id object_builder["id"] = hash
if closure: if closure:
object_builder["__closure"] = self.closure_table[obj_id] = closure object_builder["__closure"] = self.closure_table[hash] = closure
# write detached or root objects to transports # write detached or root objects to transports
if detached and self.write_transports: if detached and self.write_transports:
for t in self.write_transports: for t in self.write_transports:
t.save_object(id=obj_id, serialized_object=ujson.dumps(object_builder)) t.save_object(id=hash, serialized_object=ujson.dumps(object_builder))
del self.lineage[-1] del self.lineage[-1]
return obj_id, object_builder return hash, object_builder
def traverse_value(self, obj: Any, detach: bool = False) -> Any: def traverse_value(self, obj: Any, detach: bool = False) -> Any:
"""Decomposes a given object and constructs a serializable object or dictionary """Decomposes a given object and constructs a serializable object or dictionary
@@ -223,8 +224,8 @@ class BaseObjectSerializer:
for o in obj: for o in obj:
if isinstance(o, Base): if isinstance(o, Base):
self.detach_lineage.append(detach) self.detach_lineage.append(detach)
ref_id, _ = self._traverse_base(o) hash, _ = self._traverse_base(o)
detached_list.append(self.detach_helper(ref_id=ref_id)) detached_list.append(self.detach_helper(ref_hash=hash))
else: else:
detached_list.append(self.traverse_value(o, detach)) detached_list.append(self.traverse_value(o, detach))
return detached_list return detached_list
@@ -253,11 +254,11 @@ class BaseObjectSerializer:
return str(obj) return str(obj)
def detach_helper(self, ref_id: str) -> Dict[str, str]: def detach_helper(self, ref_hash: str) -> Dict[str, str]:
"""Helper to keep track of detached objects and their depth in the family tree and create reference objects to place in the parent object """Helper to keep track of detached objects and their depth in the family tree and create reference objects to place in the parent object
Arguments: Arguments:
ref_id {str} -- the id of the fully traversed object ref_hash {str} -- the hash of the fully traversed object
Returns: Returns:
dict -- a reference object to be inserted into the given object's parent dict -- a reference object to be inserted into the given object's parent
@@ -266,13 +267,13 @@ class BaseObjectSerializer:
for parent in self.lineage: for parent in self.lineage:
if parent not in self.family_tree: if parent not in self.family_tree:
self.family_tree[parent] = {} self.family_tree[parent] = {}
if ref_id not in self.family_tree[parent] or self.family_tree[parent][ if ref_hash not in self.family_tree[parent] or self.family_tree[parent][
ref_id ref_hash
] > len(self.detach_lineage): ] > len(self.detach_lineage):
self.family_tree[parent][ref_id] = len(self.detach_lineage) self.family_tree[parent][ref_hash] = len(self.detach_lineage)
return { return {
"referencedId": ref_id, "referencedId": ref_hash,
"speckle_type": "reference", "speckle_type": "reference",
} }
@@ -294,8 +295,6 @@ class BaseObjectSerializer:
""" """
if not obj_string: if not obj_string:
return None return None
self.deserialized = {}
obj = safe_json_loads(obj_string) obj = safe_json_loads(obj_string)
return self.recompose_base(obj=obj) return self.recompose_base(obj=obj)
@@ -314,9 +313,6 @@ class BaseObjectSerializer:
if isinstance(obj, str): if isinstance(obj, str):
obj = safe_json_loads(obj) obj = safe_json_loads(obj)
if "id" in obj and obj["id"] in self.deserialized:
return self.deserialized[obj["id"]]
if "speckle_type" in obj and obj["speckle_type"] == "reference": if "speckle_type" in obj and obj["speckle_type"] == "reference":
obj = self.get_child(obj=obj) obj = self.get_child(obj=obj)
@@ -347,14 +343,14 @@ class BaseObjectSerializer:
# 2. handle referenced child objects # 2. handle referenced child objects
elif "referencedId" in value: elif "referencedId" in value:
ref_id = value["referencedId"] ref_hash = value["referencedId"]
ref_obj_str = self.read_transport.get_object(id=ref_id) ref_obj_str = self.read_transport.get_object(id=ref_hash)
if ref_obj_str: if ref_obj_str:
ref_obj = safe_json_loads(ref_obj_str, ref_id) ref_obj = safe_json_loads(ref_obj_str, ref_hash)
base.__setattr__(prop, self.recompose_base(obj=ref_obj)) base.__setattr__(prop, self.recompose_base(obj=ref_obj))
else: else:
warnings.warn( warnings.warn(
f"Could not find the referenced child object of id `{ref_id}` in the given read transport: {self.read_transport.name}", f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}",
SpeckleWarning, SpeckleWarning,
) )
base.__setattr__(prop, self.handle_value(value)) base.__setattr__(prop, self.handle_value(value))
@@ -363,9 +359,6 @@ class BaseObjectSerializer:
else: else:
base.__setattr__(prop, self.handle_value(value)) base.__setattr__(prop, self.handle_value(value))
if "id" in obj:
self.deserialized[obj["id"]] = base
return base return base
def handle_value(self, obj: Any): def handle_value(self, obj: Any):
@@ -411,13 +404,13 @@ class BaseObjectSerializer:
return obj return obj
def get_child(self, obj: Dict): def get_child(self, obj: Dict):
ref_id = obj["referencedId"] ref_hash = obj["referencedId"]
ref_obj_str = self.read_transport.get_object(id=ref_id) ref_obj_str = self.read_transport.get_object(id=ref_hash)
if not ref_obj_str: if not ref_obj_str:
warnings.warn( warnings.warn(
f"Could not find the referenced child object of id `{ref_id}` in the given read transport: {self.read_transport.name}", f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}",
SpeckleWarning, SpeckleWarning,
) )
return obj return obj
return safe_json_loads(ref_obj_str, ref_id) return safe_json_loads(ref_obj_str, ref_hash)
+18 -2
View File
@@ -156,13 +156,29 @@ class ServerTransport(AbstractTransport):
lines = r.iter_lines(decode_unicode=True) lines = r.iter_lines(decode_unicode=True)
# iter through returned objects saving them as we go # iter through returned objects saving them as we go
target_transport.begin_write()
for line in lines: for line in lines:
if line: if line:
hash, obj = line.split("\t") hash, obj = line.split("\t")
target_transport.save_object(hash, obj) target_transport.save_object(hash, obj)
target_transport.save_object(id, root_obj_serialized) target_transport.save_object(id, root_obj_serialized)
target_transport.end_write()
return root_obj_serialized return root_obj_serialized
# async def stream_res(self, endpoint: str) -> str:
# data = b""
# async with aiohttp.ClientSession() as session:
# session.headers.update(
# {
# "Authorization": f"{self.session.headers['Authorization']}",
# "Accept": "text/plain",
# }
# )
# async with session.get(endpoint) as res:
# while True:
# chunk = await res.content.read(self.chunk_size)
# if not chunk:
# break
# data += chunk
# return data.decode("utf-8")
+1 -1
View File
@@ -188,4 +188,4 @@ class SQLiteTransport(AbstractTransport):
self.__connection = sqlite3.connect(self._root_path) self.__connection = sqlite3.connect(self._root_path)
def __del__(self): def __del__(self):
self.close() self.__connection.close()
-7
View File
@@ -65,13 +65,6 @@ def client(host, user_dict):
return client return client
@pytest.fixture(scope="session")
def second_client(host, second_user_dict):
client = SpeckleClient(host=host, use_ssl=False)
client.authenticate_with_token(second_user_dict["token"])
return client
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def sample_stream(client): def sample_stream(client):
stream = Stream( stream = Stream(
+1 -22
View File
@@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import Dict, List, Optional, Union from typing import Dict, List, Optional
from contextlib import ExitStack as does_not_raise from contextlib import ExitStack as does_not_raise
import pytest import pytest
@@ -111,7 +111,6 @@ class FrozenYoghurt(Base):
add_ons: Optional[Dict[str, float]] # dict item types won't be checked add_ons: Optional[Dict[str, float]] # dict item types won't be checked
price: float = 0.0 price: float = 0.0
dietary: DietaryRestrictions dietary: DietaryRestrictions
tag: Union[int, str]
def test_type_checking() -> None: def test_type_checking() -> None:
@@ -121,8 +120,6 @@ def test_type_checking() -> None:
order.price = "7" # will get converted order.price = "7" # will get converted
order.customer = "izzy" order.customer = "izzy"
order.dietary = DietaryRestrictions.VEGAN order.dietary = DietaryRestrictions.VEGAN
order.tag = "preorder"
order.tag = 4411
with pytest.raises(SpeckleException): with pytest.raises(SpeckleException):
order.flavours = "not a list" order.flavours = "not a list"
@@ -132,26 +129,8 @@ def test_type_checking() -> None:
order.add_ons = ["sprinkles"] order.add_ons = ["sprinkles"]
with pytest.raises(SpeckleException): with pytest.raises(SpeckleException):
order.dietary = "no nuts plz" order.dietary = "no nuts plz"
with pytest.raises(SpeckleException):
order.tag = ["tag01", "tag02"]
order.add_ons = {"sprinkles": 0.2, "chocolate": 1.0} order.add_ons = {"sprinkles": 0.2, "chocolate": 1.0}
order.flavours = ["strawberry", "lychee", "peach", "pineapple"] order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
assert order.price == 7.0 assert order.price == 7.0
def test_cached_deserialization() -> None:
material = Base(color="blue", opacity=0.5)
a = Base(name="a")
a["@material"] = material
b = Base(name="b")
b["@material"] = material
root = Base(a=a, b=b)
serialized = operations.serialize(root)
deserialized = operations.deserialize(serialized)
assert deserialized["a"]["@material"] is deserialized["b"]["@material"]
+54 -111
View File
@@ -1,5 +1,5 @@
# pylint: disable=redefined-outer-name
import json import json
from typing import Callable
import pytest import pytest
from specklepy.api import operations from specklepy.api import operations
@@ -13,9 +13,8 @@ from specklepy.objects.geometry import (
BrepEdge, BrepEdge,
BrepFace, BrepFace,
BrepLoop, BrepLoop,
BrepLoopType,
BrepTrim, BrepTrim,
BrepTrimType, BrepTrimTypeEnum,
Circle, Circle,
Curve, Curve,
Ellipse, Ellipse,
@@ -49,7 +48,12 @@ def vector():
@pytest.fixture() @pytest.fixture()
def plane(point, vector): def plane(point, vector):
return Plane(origin=point, normal=vector, xdir=vector, ydir=vector, units="m") return Plane(
origin=point,
normal=vector,
xdir=vector,
ydir=vector,
)
@pytest.fixture() @pytest.fixture()
@@ -70,7 +74,6 @@ def line(point, interval):
start=point, start=point,
end=point, end=point,
domain=interval, domain=interval,
units="none"
# These attributes are not handled in C# # These attributes are not handled in C#
# bbox=None, # bbox=None,
# length=None # length=None
@@ -78,7 +81,7 @@ def line(point, interval):
@pytest.fixture() @pytest.fixture()
def arc(plane, interval, point): def arc(plane, interval):
return Arc( return Arc(
radius=2.3, radius=2.3,
startAngle=22.1, startAngle=22.1,
@@ -87,13 +90,13 @@ def arc(plane, interval, point):
plane=plane, plane=plane,
domain=interval, domain=interval,
units="m", units="m",
startPoint=point,
midPoint=point,
endPoint=point,
# These attributes are not handled in C# # These attributes are not handled in C#
# bbox=None, # bbox=None,
# area=None, # area=None,
# length=None, # length=None,
# startPoint=None,
# midPoint=None,
# endPoint=None,
) )
@@ -233,7 +236,7 @@ def brep_edge(interval):
@pytest.fixture() @pytest.fixture()
def brep_loop(): def brep_loop():
return BrepLoop(FaceIndex=5, TrimIndices=[3, 4, 5], Type=BrepLoopType.Unknown) return BrepLoop(FaceIndex=5, TrimIndices=[3, 4, 5], Type="unknown")
@pytest.fixture() @pytest.fixture()
@@ -246,7 +249,7 @@ def brep_trim():
LoopIndex=4, LoopIndex=4,
CurveIndex=7, CurveIndex=7,
IsoStatus=6, IsoStatus=6,
TrimType=BrepTrimType.Mated, TrimType="Mated",
IsReversed=False, IsReversed=False,
# These attributes are not handled in C# # These attributes are not handled in C#
# Domain=None, # Domain=None,
@@ -335,22 +338,22 @@ def geometry_objects_dict(
], ],
) )
def test_to_and_from_list(object_name: str, geometry_objects_dict): def test_to_and_from_list(object_name: str, geometry_objects_dict):
obj = geometry_objects_dict[object_name] object = geometry_objects_dict[object_name]
assert hasattr(obj, "to_list") assert hasattr(object, "to_list")
assert hasattr(obj, "from_list") assert hasattr(object, "from_list")
chunks = obj.to_list() chunks = object.to_list()
assert isinstance(chunks, list) assert isinstance(chunks, list)
object_class = obj.__class__ object_class = object.__class__
decoded_object: Base = object_class.from_list(chunks) decoded_object: Base = object_class.from_list(chunks)
assert decoded_object.get_id() == obj.get_id() assert decoded_object.get_id() == object.get_id()
def test_brep_surfaces_value_serialization(surface): def test_brep_surfaces_value_serialization(surface):
brep = Brep() brep = Brep()
assert brep.Surfaces is None assert brep.Surfaces == None
assert brep.SurfacesValue is None assert brep.SurfacesValue == None
brep.Surfaces = [surface, surface] brep.Surfaces = [surface, surface]
assert brep.SurfacesValue == ObjectArray.from_objects([surface, surface]).data assert brep.SurfacesValue == ObjectArray.from_objects([surface, surface]).data
@@ -361,8 +364,8 @@ def test_brep_surfaces_value_serialization(surface):
def test_brep_curve2d_values_serialization(curve, polyline, circle): def test_brep_curve2d_values_serialization(curve, polyline, circle):
brep = Brep() brep = Brep()
assert brep.Curve2D is None assert brep.Curve2D == None
assert brep.Curve2DValues is None assert brep.Curve2DValues == None
brep.Curve2D = [curve, polyline] brep.Curve2D = [curve, polyline]
assert brep.Curve2DValues == CurveArray.from_curves([curve, polyline]).data assert brep.Curve2DValues == CurveArray.from_curves([curve, polyline]).data
@@ -373,8 +376,8 @@ def test_brep_curve2d_values_serialization(curve, polyline, circle):
def test_brep_curve3d_values_serialization(curve, polyline, circle): def test_brep_curve3d_values_serialization(curve, polyline, circle):
brep = Brep() brep = Brep()
assert brep.Curve3D is None assert brep.Curve3D == None
assert brep.Curve3DValues is None assert brep.Curve3DValues == None
brep.Curve3D = [curve, polyline] brep.Curve3D = [curve, polyline]
assert brep.Curve3DValues == CurveArray.from_curves([curve, polyline]).data assert brep.Curve3DValues == CurveArray.from_curves([curve, polyline]).data
@@ -386,9 +389,9 @@ def test_brep_curve3d_values_serialization(curve, polyline, circle):
def test_brep_vertices_values_serialization(): def test_brep_vertices_values_serialization():
brep = Brep() brep = Brep()
brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3] brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3]
assert brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units="mm").get_id() brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units="mm").get_id()
assert brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units="mm").get_id() brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units="mm").get_id()
assert brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units="mm").get_id() brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units="mm").get_id()
def test_trims_value_serialization(): def test_trims_value_serialization():
@@ -402,7 +405,7 @@ def test_trims_value_serialization():
0, 0,
1, 1,
1, 1,
0, 1,
1, 1,
0, 0,
0, 0,
@@ -411,82 +414,32 @@ def test_trims_value_serialization():
1, 1,
2, 2,
1, 1,
1, 0,
] ]
assert ( brep.Trims[0].get_id() == BrepTrim(
brep.Trims[0].get_id() EdgeIndex=0,
== BrepTrim( StartIndex=0,
EdgeIndex=0, EndIndex=0,
StartIndex=0, FaceIndex=0,
EndIndex=0, LoopIndex=0,
FaceIndex=0, CurveIndex=0,
LoopIndex=0, IsoStatus=1,
CurveIndex=0, TrimType=BrepTrimTypeEnum.Boundary,
IsoStatus=1, IsReversed=False,
TrimType=BrepTrimType.Boundary, ).get_id()
IsReversed=False,
).get_id()
)
assert ( brep.Trims[1].get_id() == BrepTrim(
brep.Trims[1].get_id() EdgeIndex=1,
== BrepTrim( StartIndex=0,
EdgeIndex=1, EndIndex=0,
StartIndex=0, FaceIndex=0,
EndIndex=0, LoopIndex=0,
FaceIndex=0, CurveIndex=1,
LoopIndex=0, IsoStatus=2,
CurveIndex=1, TrimType=BrepTrimTypeEnum.Boundary,
IsoStatus=2, IsReversed=True,
TrimType=BrepTrimType.Boundary, ).get_id()
IsReversed=True,
).get_id()
)
def test_loops_value_serialization():
brep = Brep()
brep.LoopsValue = [6, 0, 1, 0, 1, 2, 3]
assert brep == brep.Loops[0]._Brep # pylint: disable=protected-access
assert (
brep.Loops[0].get_id()
== BrepLoop(
FaceIndex=0, Type=BrepLoopType(1), TrimIndices=[0, 1, 2, 3]
).get_id()
)
def test_edges_value_serialization():
brep = Brep()
brep.EdgesValue = [8, 0, 0, 1, 0, -8.13345756858629, 8.13345756858629, 1, 3]
assert brep == brep.Edges[0]._Brep # pylint: disable=protected-access
assert (
brep.Edges[0].get_id()
== BrepEdge(
Curve3dIndex=0,
StartIndex=0,
EndIndex=1,
ProxyCurveIsReversed=False,
Domain=Interval(start=-8.13345756858629, end=8.13345756858629),
TrimIndices=[1, 3],
).get_id()
)
def test_faces_value_serialization():
brep = Brep()
brep.FacesValue = [4, 0, 0, 1, 0]
assert brep == brep.Faces[0]._Brep # pylint: disable=protected-access
assert (
brep.Faces[0].get_id()
== BrepFace(
SurfaceIndex=0, OuterLoopIndex=0, OrientationReversed=True, LoopIndices=[0]
).get_id()
)
def test_serialized_brep_attributes(brep: Brep): def test_serialized_brep_attributes(brep: Brep):
@@ -494,16 +447,7 @@ def test_serialized_brep_attributes(brep: Brep):
serialized = operations.serialize(brep, [transport]) serialized = operations.serialize(brep, [transport])
serialized_dict = json.loads(serialized) serialized_dict = json.loads(serialized)
removed_keys = [ removed_keys = ["Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"]
"Surfaces",
"Curve3D",
"Curve2D",
"Vertices",
"Trims",
"Loops",
"Edges",
"Faces",
]
for k in removed_keys: for k in removed_keys:
assert k not in serialized_dict.keys() assert k not in serialized_dict.keys()
@@ -515,7 +459,6 @@ def test_mesh_create():
mesh = Mesh.create(vertices, faces) mesh = Mesh.create(vertices, faces)
with pytest.raises(SpeckleException): with pytest.raises(SpeckleException):
# pylint: disable=unused-variable
bad_mesh = Mesh.create(vertices=7, faces=faces) bad_mesh = Mesh.create(vertices=7, faces=faces)
assert mesh.vertices == vertices assert mesh.vertices == vertices
+2 -10
View File
@@ -1,6 +1,5 @@
import pytest import pytest
from specklepy.api.models import ServerInfo from specklepy.api.models import ServerInfo
from specklepy.api.client import SpeckleClient
class TestServer: class TestServer:
@@ -13,19 +12,12 @@ class TestServer:
"lifespan": 9001, "lifespan": 9001,
} }
def test_server_get(self, client: SpeckleClient): def test_server_get(self, client):
server = client.server.get() server = client.server.get()
assert isinstance(server, ServerInfo) assert isinstance(server, ServerInfo)
def test_server_version(self, client: SpeckleClient): def test_server_apps(self, client):
version = client.server.version()
assert isinstance(version, tuple)
assert isinstance(version[0], int)
assert len(version) >= 3
def test_server_apps(self, client: SpeckleClient):
apps = client.server.apps() apps = client.server.apps()
assert isinstance(apps, list) assert isinstance(apps, list)
+12 -117
View File
@@ -1,18 +1,8 @@
import pytest import pytest
from datetime import datetime from datetime import datetime
from specklepy.api.models import ( from specklepy.api.models import ActivityCollection, Activity, Stream
ActivityCollection,
Activity,
PendingStreamCollaborator,
Stream,
User,
)
from specklepy.api.client import SpeckleClient from specklepy.api.client import SpeckleClient
from specklepy.logging.exceptions import ( from specklepy.logging.exceptions import GraphQLException
GraphQLException,
SpeckleException,
UnsupportedException,
)
@pytest.mark.run(order=2) @pytest.mark.run(order=2)
@@ -35,10 +25,6 @@ class TestStream:
isPublic=False, isPublic=False,
) )
@pytest.fixture(scope="module")
def second_user(self, second_client: SpeckleClient):
return second_client.user.get()
def test_stream_create(self, client, stream, updated_stream): def test_stream_create(self, client, stream, updated_stream):
stream.id = updated_stream.id = client.stream.create( stream.id = updated_stream.id = client.stream.create(
name=stream.name, name=stream.name,
@@ -93,67 +79,22 @@ class TestStream:
assert isinstance(favorited, Stream) assert isinstance(favorited, Stream)
assert unfavorited.favoritedDate is None assert unfavorited.favoritedDate is None
def test_stream_grant_permission(self, client, stream, second_user): def test_stream_grant_permission(self, client, stream, second_user_dict):
# deprecated as of Speckle Server 2.6.4 granted = client.stream.grant_permission(
with pytest.raises(UnsupportedException):
client.stream.grant_permission(
stream_id=stream.id,
user_id=second_user.id,
role="stream:contributor",
)
def test_stream_invite(
self, client: SpeckleClient, stream: Stream, second_user_dict: dict
):
invited = client.stream.invite(
stream_id=stream.id, stream_id=stream.id,
email=second_user_dict["email"], user_id=second_user_dict["id"],
role="stream:reviewer", role="stream:contributor",
message="welcome to my stream!",
) )
assert invited is True fetched_stream = client.stream.get(stream.id)
# fail if no email or id assert granted is True
with pytest.raises(SpeckleException): assert len(fetched_stream.collaborators) == 2
client.stream.invite(stream_id=stream.id) assert fetched_stream.collaborators[0].name == second_user_dict["name"]
def test_stream_invite_get_all_for_user( def test_stream_revoke_permission(self, client, stream, second_user_dict):
self, second_client: SpeckleClient, stream: Stream
):
# NOTE: these are user queries, but testing here to contain the flow
invites = second_client.user.get_all_pending_invites()
assert isinstance(invites, list)
assert isinstance(invites[0], PendingStreamCollaborator)
assert len(invites) == 1
invite = second_client.user.get_pending_invite(stream_id=stream.id)
assert isinstance(invite, PendingStreamCollaborator)
def test_stream_invite_use(self, second_client: SpeckleClient, stream: Stream):
invite: PendingStreamCollaborator = (
second_client.user.get_all_pending_invites()[0]
)
accepted = second_client.stream.invite_use(
stream_id=stream.id, token=invite.token
)
assert accepted is True
def test_stream_update_permission(
self, client: SpeckleClient, stream: Stream, second_user: User
):
updated = client.stream.update_permission(
stream_id=stream.id, user_id=second_user.id, role="stream:contributor"
)
assert updated is True
def test_stream_revoke_permission(self, client, stream, second_user):
revoked = client.stream.revoke_permission( revoked = client.stream.revoke_permission(
stream_id=stream.id, user_id=second_user.id stream_id=stream.id, user_id=second_user_dict["id"]
) )
fetched_stream = client.stream.get(stream.id) fetched_stream = client.stream.get(stream.id)
@@ -161,52 +102,6 @@ class TestStream:
assert revoked is True assert revoked is True
assert len(fetched_stream.collaborators) == 1 assert len(fetched_stream.collaborators) == 1
def test_stream_invite_cancel(
self,
client: SpeckleClient,
stream: Stream,
second_user: User,
):
invited = client.stream.invite(
stream_id=stream.id,
user_id=second_user.id,
message="welcome to my stream!",
)
assert invited is True
invites = client.stream.get_all_pending_invites(stream_id=stream.id)
cancelled = client.stream.invite_cancel(
invite_id=invites[0].inviteId, stream_id=stream.id
)
assert cancelled is True
def test_stream_invite_batch(
self, client: SpeckleClient, stream: Stream, second_user: User
):
# NOTE: only works for server admins
# invited = client.stream.invite_batch(
# stream_id=stream.id,
# emails=["userA@speckle.xyz", "userB@speckle.xyz"],
# user_ids=[second_user.id],
# message="yeehaw 🤠",
# )
# assert invited is True
# invited_only_email = client.stream.invite_batch(
# stream_id=stream.id,
# emails=["userC@speckle.xyz"],
# message="yeehaw 🤠",
# )
# assert invited_only_email is True
# fail if no emails or user ids
with pytest.raises(SpeckleException):
client.stream.invite_batch(stream_id=stream.id)
def test_stream_activity(self, client: SpeckleClient, stream: Stream): def test_stream_activity(self, client: SpeckleClient, stream: Stream):
activity = client.stream.activity(stream.id) activity = client.stream.activity(stream.id)
-158
View File
@@ -1,158 +0,0 @@
import json
from typing import Callable
import pytest
from specklepy.api import operations
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.objects.encoding import CurveArray, ObjectArray
from specklepy.objects.geometry import (
Line,
Mesh,
Point,
Vector,
)
from specklepy.transports.memory import MemoryTransport
from specklepy.objects.structural.geometry import (
Node,
Element1D,
Element2D,
Restraint,
ElementType1D,
ElementType2D,
)
from specklepy.objects.structural.properties import (
Property1D,
Property2D,
SectionProfile,
MemberType,
ShapeType,
)
from specklepy.objects.structural.material import (
Material,
)
from specklepy.objects.structural.analysis import Model
from specklepy.objects.structural.loading import LoadGravity
@pytest.fixture()
def point():
return Point(x=1, y=10, z=0)
@pytest.fixture()
def vector():
return Vector(x=0, y=0, z=-1)
@pytest.fixture()
def line(point, interval):
return Line(
start=point,
end=point,
domain=interval,
# These attributes are not handled in C#
# bbox=None,
# length=None
)
@pytest.fixture()
def mesh(box):
return Mesh(
vertices=[2, 1, 2, 4, 77.3, 5, 33, 4, 2],
faces=[1, 2, 3, 4, 5, 6, 7],
colors=[111, 222, 333, 444, 555, 666, 777],
bbox=box,
area=233,
volume=232.2,
)
@pytest.fixture()
def restraint():
return Restraint(code="FFFFFF")
@pytest.fixture()
def node(restraint, point):
return Node(basePoint=point, restraint=restraint, name="node1")
@pytest.fixture()
def material():
return Material(name="TestMaterial")
@pytest.fixture()
def memberType():
return MemberType(0)
@pytest.fixture()
def shapeType():
return ShapeType(8)
@pytest.fixture()
def sectionProfile(shapeType):
return SectionProfile(name="Test", shapeType=shapeType)
@pytest.fixture()
def property1D(memberType, sectionProfile, material):
return Property1D(
Material=material,
SectionProfile=sectionProfile,
memberType=memberType,
)
@pytest.fixture()
def elementType1D():
return ElementType1D(0)
@pytest.fixture()
def element1D(line, restraint, elementType1D, property1D):
return Element1D(
baseLine=line,
end1Releases=restraint,
end2Releases=restraint,
type=elementType1D,
property=property1D,
)
@pytest.fixture()
def property2D(material):
return Property2D(Material=material)
@pytest.fixture()
def elementType2D():
return ElementType2D(0)
@pytest.fixture()
def element2D(point, elementType2D):
return Element2D(
topology=[point],
type=elementType2D,
)
@pytest.fixture()
def loadGravity(element1D, element2D, vector):
return LoadGravity(elements=[element1D, element2D], gravityFactors=vector)
@pytest.fixture()
def model(loadGravity, element1D, element2D, material, property1D, property2D):
return Model(
loads=[loadGravity],
elements=[element1D, element2D],
materials=[material],
properties=[property1D, property2D],
)