Compare commits

..

1 Commits

Author SHA1 Message Date
izzy lyseggen 3246edda8f feat(objects): add value property to points 2021-06-17 18:23:35 +01:00
24 changed files with 196 additions and 664 deletions
+8 -11
View File
@@ -4,7 +4,7 @@ orbs:
python: circleci/python@1.3.2
jobs:
test:
build:
docker:
- image: "cimg/python:<<parameters.tag>>"
- image: "circleci/node:12"
@@ -14,7 +14,7 @@ jobs:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
- image: "speckle/speckle-server"
- image: "speckle/speckle-server:5f8cf11cba07ea6a54000243f9cb343b61cbba13"
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "localhost"
@@ -38,30 +38,27 @@ jobs:
name: upgrade pip
- python/install-packages:
pkg-manager: poetry
- run: poetry run pytest
- run: poetry run pytest --version
deploy:
docker:
- image: "circleci/python:3.8"
steps:
- checkout
- run: python patch_version.py $CIRCLE_TAG
- run: poetry build
- run: poetry publish -u specklesystems -p $PYPI_PASSWORD
workflows:
main:
jobs:
- test:
jobs:
- build:
matrix:
parameters:
tag: ["3.6", "3.7", "3.8", "3.9"]
filters:
tags:
only: /.*/
publish:
jobs:
- deploy:
requires:
- test
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
+1 -1
View File
@@ -45,7 +45,7 @@ from specklepy.api.credentials import get_default_account, get_local_accounts
all_accounts = get_local_accounts() # get back a list
account = get_default_account()
client = SpeckleClient(host="speckle.xyz")
client = SpeckleClient(host="localhost:3000", use_ssl=False)
# client = SpeckleClient(host="yourserver.com") or whatever your host is
client.authenticate(account.token)
-31
View File
@@ -1,31 +0,0 @@
import re
import sys
def patch(tag):
print(f"Patching version: {tag}")
with open("pyproject.toml", "r") as f:
lines = f.readlines()
if "version" not in lines[2]:
raise Exception(f"Invalid pyproject.toml. Could not patch version.")
lines[2] = f'version = "{tag}"\n'
with open("pyproject.toml", "w") as file:
file.writelines(lines)
def main():
if len(sys.argv) < 2:
return
tag = sys.argv[1]
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
raise ValueError(f"Invalid tag provided: {tag}")
patch(tag)
if __name__ == "__main__":
main()
Generated
+1 -32
View File
@@ -371,14 +371,6 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "ujson"
version = "4.1.0"
description = "Ultra fast JSON encoder and decoder for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "urllib3"
version = "1.26.5"
@@ -428,7 +420,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake
[metadata]
lock-version = "1.1"
python-versions = "^3.6.5"
content-hash = "728db0014dfb8a83c50fe5ce6e86d068c4c87d319d50fb1e8135e63507713f30"
content-hash = "84e846c1bb02924ceada07406e95032e0632d229a36657ba2b85129e68f1526d"
[metadata.files]
aiohttp = [
@@ -719,29 +711,6 @@ typing-extensions = [
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
ujson = [
{file = "ujson-4.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:148680f2bc6e52f71c56908b65f59b36a13611ac2f75a86f2cb2bce2b2c2588c"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c2fb32976982e4e75ca0843a1e7b2254b8c5d8c45d979ebf2db29305b4fa31"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:971d4b450e689bfec8ad6b22060fb9b9bec1e0860dbdf0fa7cfe4068adbc5f58"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f453480b275192ae40ef350a4e8288977f00b02e504ed34245ebd12d633620cb"},
{file = "ujson-4.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f135db442e5470d9065536745968efc42a60233311c8509b9327bcd59a8821c7"},
{file = "ujson-4.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:2251fc9395ba4498cbdc48136a179b8f20914fa8b815aa9453b20b48ad120f43"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9005d0d952d0c1b3dff5cdb79df2bde35a3499e2de3f708a22c45bbb4089a1f6"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:117855246a9ea3f61f3b69e5ca1b1d11d622b3126f50a0ec08b577cb5c87e56e"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:989bed422e7e20c7ba740a4e1bbeb28b3b6324e04f023ea238a2e5449fc53668"},
{file = "ujson-4.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:44993136fd2ecade747b6db95917e4f015a3279e09a08113f70cbbd0d241e66a"},
{file = "ujson-4.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9e962df227fd1d851ff095382a9f8432c2470c3ee640f02ae14231dc5728e6f3"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6013cda610c5149fb80a84ee815b210aa2e7fe4edf1d2bce42c02336715208"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41b7e5422184249b5b94d1571206f76e5d91e8d721ce51abe341a88f41dd6692"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:807bb0585f30a650ec981669827721ed3ee1ee24f2c6f333a64982a40eb66b82"},
{file = "ujson-4.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d2955dd5cce0e76ba56786d647aaedca2cebb75eda9f0ec1787110c3646751a8"},
{file = "ujson-4.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a873c93d43f9bd14d9e9a6d2c6eb7aae4aad9717fe40c748d0cd4b6ed7767c62"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8fe9bbeca130debb10eea7910433a0714c8efc057fad36353feccb87c1d07f"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81a49dbf176ae041fc86d2da564f5b9b46faf657306035632da56ecfd7203193"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1fb2455e62f20ab4a6d49f78b5dc4ff99c72fdab9466e761120e9757fa35f4d7"},
{file = "ujson-4.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:44db30b8fc52e70a6f67def11804f74818addafef0a65cd7f0abb98b7830920f"},
{file = "ujson-4.1.0.tar.gz", hash = "sha256:22b63ec4409f0d2f2c4c9d5aa331997e02470b7a15a3233f3cc32f2f9b92d58c"},
]
urllib3 = [
{file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"},
{file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"},
+1 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.1.0"
version = "2.2.3"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -15,7 +15,6 @@ python = "^3.6.5"
pydantic = "^1.7.3"
appdirs = "^1.4.4"
gql = {version = ">=3.0.0a6", extras = ["all"], allow-prereleases = true}
ujson = "^4.1.0"
[tool.poetry.dev-dependencies]
black = "^20.8b1"
-32
View File
@@ -21,33 +21,6 @@ from gql.transport.websockets import WebsocketsTransport
class SpeckleClient:
"""
The `SpeckleClient` is your entry point for interacting with your Speckle Server's GraphQL API.
You'll need to have access to a server to use it, or you can use our public server `speckle.xyz`.
To authenticate the client, you'll need to have downloaded the [Speckle Manager](https://speckle.guide/#speckle-manager)
and added your account.
```py
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with a token (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate(token=account.token)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
# use that stream id to get the stream from the server
new_stream = client.stream.get(id=new_stream_id)
```
"""
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
@@ -82,11 +55,6 @@ class SpeckleClient:
except Exception as ex:
raise SpeckleException(f"{self.url} is not a compatible Speckle Server", ex)
def __repr__(self):
return (
f"SpeckleClient( server: {self.url}, authenticated: {self.me is not None} )"
)
def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL entrypoint is created
+2 -103
View File
@@ -1,13 +1,9 @@
import os
from specklepy.transports.server.server import ServerTransport
from warnings import warn
from pydantic import BaseModel
from typing import List, Optional
from urllib.parse import urlparse, unquote
from pydantic import BaseModel
from specklepy.api.models import ServerInfo
from specklepy.api.client import SpeckleClient
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.logging.exceptions import SpeckleException
class UserInfo(BaseModel):
@@ -83,100 +79,3 @@ def get_default_account(base_path: str = None) -> Account:
default.isDefault = True
return default
class StreamWrapper:
stream_url: str = None
use_ssl: bool = True
host: str = None
stream_id: str = None
commit_id: str = None
object_id: str = None
branch_name: str = None
client: SpeckleClient = None
account: Account = None
def __repr__(self):
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
def __str__(self) -> str:
return self.__repr__()
@property
def type(self) -> str:
if self.object_id:
return "object"
elif self.commit_id:
return "commit"
elif self.branch_name:
return "branch"
else:
return "stream" if self.stream_id else "invalid"
def __init__(self, url: str) -> None:
self.stream_url = url
parsed = urlparse(url)
self.host = parsed.netloc
self.use_ssl = parsed.scheme == "https"
segments = parsed.path.strip("/").split("/")
if not segments or len(segments) > 4 or len(segments) < 2:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
while segments:
segment = segments.pop(0)
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
def get_account(self) -> Account:
if self.account:
return self.account
self.account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
None,
)
return self.account
def get_client(self) -> SpeckleClient:
if self.client:
return self.client
if not self.account:
self.get_account()
self.client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if self.account is None:
warn(f"No local account found for server {self.host}", SpeckleWarning)
return self.client
self.client.authenticate(self.account.token)
return self.client
def get_transport(self) -> ServerTransport:
if not self.client:
self.get_client()
return ServerTransport(self.client, self.stream_id)
-1
View File
@@ -22,7 +22,6 @@ class Commit(BaseModel):
authorName: Optional[str]
authorId: Optional[str]
authorAvatar: Optional[str]
branchName: Optional[str]
createdAt: Optional[str]
sourceApplication: Optional[str]
referencedObject: Optional[str]
+5 -6
View File
@@ -1,3 +1,4 @@
import json
from typing import List
from specklepy.objects.base import Base
from specklepy.transports.sqlite import SQLiteTransport
@@ -51,8 +52,7 @@ def receive(
Arguments:
obj_id {str} -- the id of the object to receive
remote_transport {Transport} -- the transport to receive from
local_transport {Transport} -- the local cache to check for existing objects
(defaults to `SQLiteTransport`)
local_transport {Transport} -- the transport to send from
Returns:
Base -- the base object
@@ -97,7 +97,9 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
return serializer.write_json(base)[1]
def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Base:
def deserialize(
obj_string: str, read_transport: AbstractTransport = SQLiteTransport()
) -> Base:
"""
Deserialize a string object into a Base object. If the object contains referenced child objects that are not stored in the local db, a read transport needs to be provided in order to recompose the base with the children objects.
@@ -109,9 +111,6 @@ def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Ba
Returns:
Base -- the deserialized object
"""
if not read_transport:
read_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
+3 -8
View File
@@ -1,10 +1,9 @@
from specklepy.transports.sqlite import SQLiteTransport
from logging import error
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from typing import Dict, List
from gql.client import Client
from gql.gql import gql
from gql.transport.exceptions import TransportQueryError
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
class ResourceBase(object):
@@ -41,11 +40,7 @@ class ResourceBase(object):
if schema:
return schema.parse_obj(response)
elif self.schema:
try:
return self.schema.parse_obj(response)
except:
s = BaseObjectSerializer(read_transport=SQLiteTransport())
return s.recompose_base(response)
return self.schema.parse_obj(response)
else:
return response
+1 -1
View File
@@ -1,5 +1,6 @@
from typing import Optional, List
from gql import gql
from pydantic.main import BaseModel
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Commit
@@ -39,7 +40,6 @@ class Resource(ResourceBase):
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
-5
View File
@@ -28,8 +28,3 @@ class GraphQLException(SpeckleException):
def __str__(self) -> str:
return f"GraphQLException: {self.message}"
class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None:
super().__init__(*args)
+62 -210
View File
@@ -1,71 +1,15 @@
import typing
from warnings import warn
from typing import get_type_hints
from inspect import getattr_static
from pydantic import BaseModel, validator
from pydantic.main import Extra
from typing import ClassVar, Dict, List, Optional, Any, Set, Type
from specklepy.transports.memory import MemoryTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.units import get_units_from_string
PRIMITIVES = (int, float, str, bool)
# to remove from dir() when calling get_member_names()
REMOVE_FROM_DIR = {
"Config",
"_Base__dict_helper",
"__annotations__",
"__class__",
"__delattr__",
"__dict__",
"__dir__",
"__doc__",
"__eq__",
"__format__",
"__ge__",
"__getattribute__",
"__getitem__",
"__gt__",
"__hash__",
"__init__",
"__init_subclass__",
"__le__",
"__lt__",
"__module__",
"__ne__",
"__new__",
"__reduce__",
"__reduce_ex__",
"__repr__",
"__setattr__",
"__setitem__",
"__sizeof__",
"__str__",
"__subclasshook__",
"__weakref__",
"_chunk_size_default",
"_chunkable",
"_count_descendants",
"_attr_types",
"_detachable",
"_handle_object_count",
"_type_check",
"_type_registry",
"_units",
"add_chunkable_attrs",
"add_detachable_attrs",
"get_children_count",
"get_dynamic_member_names",
"get_id",
"get_member_names",
"get_registered_type",
"get_typed_member_names",
"to_dict",
"update_forward_refs",
"validate_prop_name",
}
class _RegisteringBase:
class _RegisteringBase(BaseModel):
"""
Private Base model for Speckle types.
@@ -77,8 +21,7 @@ class _RegisteringBase:
"""
speckle_type: ClassVar[str]
_type_registry: ClassVar[Dict[str, "Base"]] = {}
_attr_types: ClassVar[Dict[str, Type]] = {}
_type_registry: ClassVar[Dict[str, Type["Base"]]] = {}
class Config:
validate_assignment = True
@@ -90,9 +33,7 @@ class _RegisteringBase:
def __init_subclass__(
cls,
speckle_type: str = None,
chunkable: Dict[str, int] = None,
detachable: Set[str] = None,
speckle_type: Optional[str] = None,
**kwargs: Dict[str, Any],
):
"""
@@ -110,15 +51,6 @@ class _RegisteringBase:
)
cls.speckle_type = speckle_type or cls.__name__
cls._type_registry[cls.speckle_type] = cls # type: ignore
try:
cls._attr_types = get_type_hints(cls)
except Exception:
cls._attr_types = getattr(cls, "__annotations__", {})
if chunkable:
chunkable = {k: v for k, v in chunkable.items() if isinstance(v, int)}
cls._chunkable = dict(cls._chunkable, **chunkable)
if detachable:
cls._detachable = cls._detachable.union(detachable)
super().__init_subclass__(**kwargs)
@@ -131,11 +63,6 @@ class Base(_RegisteringBase):
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
def __init__(self, **kwargs) -> None:
super().__init__()
for k, v in kwargs.items():
self.__setattr__(k, v)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(id: {self.id}, "
@@ -146,23 +73,6 @@ class Base(_RegisteringBase):
def __str__(self) -> str:
return self.__repr__()
@classmethod
def of_type(cls, speckle_type: str, **kwargs) -> "Base":
"""
Get a plain Base object with a specified speckle_type.
The speckle_type is protected and cannot be overwritten on a class instance.
This is to prevent problems with receiving in other platforms or connectors.
However, if you really need a base with a different type, here is a helper
to do that for you.
This is used in the deserialisation of unknown types so their speckle_type
can be preserved.
"""
b = cls(**kwargs)
b.__dict__.update(speckle_type=speckle_type)
return b
def __setitem__(self, name: str, value: Any) -> None:
self.validate_prop_name(name)
self.__dict__[name] = value
@@ -172,46 +82,23 @@ class Base(_RegisteringBase):
def __setattr__(self, name: str, value: Any) -> None:
"""
Type checking, guard attribute, and property set mechanism.
Guard attribute and property set mechanism.
The `speckle_type` is a protected class attribute it must not be overridden.
This also performs a type check if the attribute is type hinted.
"""
if name == "speckle_type":
# not sure if we should raise an exception here??
# raise SpeckleException(
# "Cannot override the `speckle_type`. This is set manually by the class or on deserialisation"
# )
return
# if value is not None:
value = self._type_check(name, value)
attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
try:
attr.__set__(self, value)
except AttributeError:
pass # the prop probably doesn't have a setter
super().__setattr__(name, value)
@classmethod
def update_forward_refs(cls) -> None:
"""
Attempts to populate the internal defined types dict for type checking sometime after defining the class.
This is already done when defining the class, but can be called again if references to undefined types were
included.
See `objects.geometry` for an example of how this is used with the Brep class definitions
"""
try:
cls._attr_types = get_type_hints(cls)
except Exception as e:
warn(f"Could not update forward refs for class {cls.__name__}: {e}")
if name != "speckle_type":
attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
try:
attr.__set__(self, value)
except AttributeError:
pass # the prop probably doesn't have a setter
super().__setattr__(name, value)
@classmethod
def validate_prop_name(cls, name: str) -> None:
"""Validator for dynamic attribute names."""
if name in {"", "@"}:
if name in ("", "@"):
raise ValueError("Invalid Name: Base member names cannot be empty strings")
if name.startswith("@@"):
raise ValueError(
@@ -222,45 +109,6 @@ class Base(_RegisteringBase):
"Invalid Name: Base member names cannot contain characters '.' or '/'",
)
def _type_check(self, name: str, value: Any):
"""
Lightweight type checking of values before setting them
NOTE: Does not check subscripted types within generics as the performance hit of checking
each item within a given collection isn't worth it. Eg if you have a type Dict[str, float],
we will only check if the value you're trying to set is a dict.
"""
types = getattr(self, "_attr_types", {})
t = types.get(name, None)
if t is None:
return value
if t.__module__ == "typing":
origin = getattr(t, "__origin__")
t = t.__args__ if origin is typing.Union else origin
if not isinstance(t, (type, tuple)):
warn(
f"Unrecognised type '{t}' provided for attribute '{name}'. Type will not been validated."
)
return value
if isinstance(value, t):
return value
# to be friendly, we'll parse ints and strs into floats, but not the other way around
# (to avoid unexpected rounding)
if t is float and isinstance(value, (int, str, float)):
try:
return float(value)
except ValueError:
pass
if t is str and value is not None:
return str(value)
raise SpeckleException(
f"Cannot set '{self.__class__.__name__}.{name}': it expects type '{t.__name__}', but received type '{type(value).__name__}'"
)
def add_chunkable_attrs(self, **kwargs: int) -> None:
"""
Mark defined attributes as chunkable for serialisation
@@ -288,50 +136,53 @@ class Base(_RegisteringBase):
def units(self, value: str):
self._units = get_units_from_string(value)
# def to_dict(self) -> Dict[str, Any]:
# """Convenience method to view the whole base object as a dict"""
# base_dict = self.__dict__
# for key, value in base_dict.items():
# if not value or isinstance(value, PRIMITIVES):
# continue
# else:
# base_dict[key] = self.__dict_helper(value)
# return base_dict
def to_dict(self) -> Dict[str, Any]:
"""Convenience method to view the whole base object as a dict"""
base_dict = self.__dict__
for key, value in base_dict.items():
if not value or isinstance(value, PRIMITIVES):
continue
else:
base_dict[key] = self.__dict_helper(value)
return base_dict
# def __dict_helper(self, obj: Any) -> Any:
# if not obj or isinstance(obj, PRIMITIVES):
# return obj
# if isinstance(obj, Base):
# return self.__dict_helper(obj.__dict__)
# if isinstance(obj, (list, set)):
# return [self.__dict_helper(v) for v in obj]
# if not isinstance(obj, dict):
# raise SpeckleException(
# message=f"Could not convert to dict due to unrecognized type: {type(obj)}"
# )
def __dict_helper(self, obj: Any) -> Any:
if not obj or isinstance(obj, PRIMITIVES):
return obj
if isinstance(obj, Base):
return self.__dict_helper(obj.__dict__)
if isinstance(obj, (list, set)):
return [self.__dict_helper(v) for v in obj]
if not isinstance(obj, dict):
raise SpeckleException(
message=f"Could not convert to dict due to unrecognized type: {type(obj)}"
)
# for k, v in obj.items():
# if v and not isinstance(obj, PRIMITIVES):
# obj[k] = self.__dict_helper(v)
# return obj
for k, v in obj.items():
if v and not isinstance(obj, PRIMITIVES):
obj[k] = self.__dict_helper(v)
return obj
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
# attrs = set(self.__dict__.keys())
attr_dir = list(set(dir(self)) - REMOVE_FROM_DIR)
return [
attrs = list(self.__dict__.keys())
properties = [
name
for name in attr_dir
if not name.startswith("_") and not callable(getattr(self, name))
for name in dir(self)
if not name.startswith("_")
and name
!= "fields" # soon to be removed as this pydantic prop is depreciated
and isinstance(getattr(self, name, None), property)
]
return attrs + properties
def get_typed_member_names(self) -> List[str]:
"""Get all of the names of the defined (typed) properties of this object"""
return list(self._attr_types.keys())
return list(self.__fields__.keys())
def get_dynamic_member_names(self) -> List[str]:
"""Get all of the names of the dynamic properties of this object"""
return list(set(self.__dict__.keys()) - set(self._attr_types.keys()))
return list(set(self.__dict__.keys()) - set(self.__fields__.keys()))
def get_children_count(self) -> int:
"""Get the total count of children Base objects"""
@@ -364,11 +215,15 @@ class Base(_RegisteringBase):
return 0
parsed.append(base)
return sum(
self._handle_object_count(value, parsed)
for name, value in base.get_member_names()
if not name.startswith("@")
)
count = 0
for name, value in base.__dict__.items():
if name.startswith("@"):
continue
else:
count += self._handle_object_count(value, parsed)
return count
def _handle_object_count(self, obj: Any, parsed: List) -> int:
count = 0
@@ -394,12 +249,9 @@ class Base(_RegisteringBase):
count += self._handle_object_count(value, parsed)
return count
Base.update_forward_refs()
class Config:
extra = Extra.allow
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: List[Any] = None
def __init__(self) -> None:
self.data = []
data: List[Any] = []
+5 -10
View File
@@ -14,12 +14,7 @@ CHUNKABLE_PROPS = {
DETACHABLE = {"detach_this", "origin", "detached_list"}
class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
pointslist: List[Base] = None
dots: List[int] = None
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
class FakeMesh(Base):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
@@ -29,10 +24,10 @@ class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
detached_list: List[Base] = None
_origin: Point = None
# def __init__(self, **kwargs) -> None:
# super(FakeMesh, self).__init__(**kwargs)
# self.add_chunkable_attrs(**CHUNKABLE_PROPS)
# self.add_detachable_attrs(DETACHABLE)
def __init__(self, **kwargs) -> None:
super(FakeMesh, self).__init__(**kwargs)
self.add_chunkable_attrs(**CHUNKABLE_PROPS)
self.add_detachable_attrs(DETACHABLE)
@property
def origin(self):
+60 -65
View File
@@ -5,26 +5,28 @@ GEOMETRY = "Objects.Geometry."
class Interval(Base, speckle_type="Objects.Primitive.Interval"):
start: float = 0.0
end: float = 0.0
start: float = 0
end: float = 0
def length(self):
return abs(self.start - self.end)
class Point(Base, speckle_type=GEOMETRY + "Point"):
x: float = 0.0
y: float = 0.0
z: float = 0.0
x: float = 0
y: float = 0
z: float = 0
def __init__(self, x: float = 0, y: float = 0, z: float = 0, **data: Any) -> None:
super().__init__(**data)
self.x, self.y, self.z = x, y, z
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id: {self.id}, speckle_type: {self.speckle_type})"
@classmethod
def from_coords(x: float = 0.0, y: float = 0.0, z: float = 0.0):
pt = Point()
pt.x, pt.y, pt.z = x, y, z
return pt
@property
def value(self) -> List[float]:
return [self.x, self.y, self.z]
class Vector(Point, speckle_type=GEOMETRY + "Vector"):
@@ -94,7 +96,7 @@ class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
length: float = None
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
value: List[float] = None
closed: bool = None
domain: Interval = None
@@ -102,11 +104,14 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
area: float = None
length: float = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_chunkable_attrs(value=20000)
@classmethod
def from_points(cls, points: List[Point]):
polyline = cls()
polyline.units = points[0].units
polyline.value = []
for point in points:
polyline.value.extend([point.x, point.y, point.z])
return polyline
@@ -130,16 +135,10 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.value)
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
return [Point(v, next(values), next(values), units=self.units) for v in values]
class Curve(
Base,
speckle_type=GEOMETRY + "Curve",
chunkable={"points": 20000, "weights": 20000, "knots": 20000},
):
class Curve(Base, speckle_type=GEOMETRY + "Curve"):
degree: int = None
periodic: bool = None
rational: bool = None
@@ -153,6 +152,10 @@ class Curve(
area: float = None
length: float = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_chunkable_attrs(points=20000, weights=20000, knots=20000)
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.points:
@@ -162,13 +165,11 @@ class Curve(
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.points)
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
return [Point(v, next(values), next(values), units=self.units) for v in values]
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: List[Base] = None
segments: List[Base] = []
domain: Interval = None
closed: bool = None
bbox: Box = None
@@ -190,16 +191,7 @@ class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
bbox: Box = None
class Mesh(
Base,
speckle_type=GEOMETRY + "Mesh",
chunkable={
"vertices": 2000,
"faces": 2000,
"colors": 2000,
"textureCoordinates": 2000,
},
):
class Mesh(Base, speckle_type=GEOMETRY + "Mesh"):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
@@ -208,6 +200,12 @@ class Mesh(
area: float = None
volume: float = None
def __init__(self, **data) -> None:
super().__init__(**data)
self.add_chunkable_attrs(
vertices=2000, faces=2000, colors=2000, textureCoordinates=2000
)
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: int = None
@@ -237,8 +235,7 @@ class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
@property
def _loops(self):
if self.LoopIndices:
return [self._Brep.Loops[i] for i in self.LoopIndices]
return [self._Brep.Loops[index] for index in self.LoopIndices]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
@@ -260,8 +257,7 @@ class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
@property
def _trims(self):
if self.TrimIndices:
return [self._Brep.Trims[i] for i in self.TrimIndices]
return [self._Brep.Trims[i] for i in self.TrimIndices]
@property
def _curve(self):
@@ -280,8 +276,7 @@ class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
@property
def _trims(self):
if self.TrimIndices:
return [self._Brep.Trims[i] for i in self.TrimIndices]
return [self._Brep.Trims[i] for i in self.TrimIndices]
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
@@ -314,41 +309,41 @@ class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
return self._Brep.Curve2D[self.CurveIndex]
class Brep(
Base,
speckle_type=GEOMETRY + "Brep",
chunkable={
"Surfaces": 200,
"Curve3D": 200,
"Curve2D": 200,
"Vertices": 5000,
"Edges": 5000,
"Loops": 5000,
"Trims": 5000,
"Faces": 5000,
},
detachable={"displayValue"},
):
class Brep(Base, speckle_type=GEOMETRY + "Brep"):
provenance: str = None
bbox: Box = None
area: float = None
volume: float = None
displayValue: Mesh = None
Surfaces: List[Surface] = None
Curve3D: List[Base] = None
Curve2D: List[Base] = None
Vertices: List[Point] = None
Edges: List[BrepEdge] = None
Loops: List[BrepLoop] = None
Trims: List[BrepTrim] = None
Faces: List[BrepFace] = None
Surfaces: List[Surface] = []
Curve3D: List[Base] = []
Curve2D: List[Base] = []
Vertices: List[Point] = []
Edges: List[BrepEdge] = []
Loops: List[BrepLoop] = []
Trims: List[BrepTrim] = []
Faces: List[BrepFace] = []
IsClosed: bool = None
Orientation: int = None
Orientation: int = 0
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_detachable_attrs({"displayValue"})
self.add_chunkable_attrs(
Surfaces=200,
Curve3D=200,
Curve2D=200,
Vertices=5000,
Edges=5000,
Loops=5000,
Trims=5000,
Faces=5000,
)
def __setattr__(self, name: str, value: Any) -> None:
if not value:
return
if name in {"Edges", "Loops", "Trims", "Faces"}:
if name in ["Edges", "Loops", "Trims", "Faces"]:
for val in value:
val._Brep = self
super().__setattr__(name, value)
-1
View File
@@ -6,7 +6,6 @@ UNITS_STRINGS = {
"mm": ["mm", "mil", "millimeters", "millimetres"],
"cm": ["cm", "centimetre", "centimeter", "centimetres", "centimeters"],
"m": ["m", "meter", "meters", "metre", "metres"],
"km": ["km", "kilometer", "kilometre", "kilometers", "kilometres"],
"in": ["in", "inch", "inches"],
"ft": ["ft", "foot", "feet"],
"yd": ["yd", "yard", "yards"],
@@ -1,4 +1,4 @@
import ujson
import json
import hashlib
import re
@@ -14,7 +14,7 @@ PRIMITIVES = (int, float, str, bool)
def hash_obj(obj: Any) -> str:
return hashlib.sha256(ujson.dumps(obj).encode()).hexdigest()[:32]
return hashlib.sha256(json.dumps(obj).encode()).hexdigest()[:32]
class BaseObjectSerializer:
@@ -35,7 +35,7 @@ class BaseObjectSerializer:
self.__reset_writer()
self.detach_lineage = [True]
hash, obj = self.traverse_base(base)
return hash, ujson.dumps(obj)
return hash, json.dumps(obj)
def traverse_base(self, base: Base) -> Tuple[str, Dict]:
"""Decomposes the given base object and builds a serializable dictionary
@@ -70,9 +70,7 @@ class BaseObjectSerializer:
# only bother with chunking and detaching if there is a write transport
if self.write_transports:
dynamic_chunk_match = prop.startswith("@") and re.match(
r"^@\((\d*)\)", prop
)
dynamic_chunk_match = re.match(r"^@\((\d*)\)", prop)
if dynamic_chunk_match:
chunk_size = dynamic_chunk_match.groups()[0]
base._chunkable[prop] = (
@@ -142,7 +140,7 @@ class BaseObjectSerializer:
# write detached or root objects to transports
if detached and self.write_transports:
for t in self.write_transports:
t.save_object(id=hash, serialized_object=ujson.dumps(object_builder))
t.save_object(id=hash, serialized_object=json.dumps(object_builder))
del self.lineage[-1]
@@ -238,7 +236,7 @@ class BaseObjectSerializer:
"""
if not obj_string:
return None
obj = ujson.loads(obj_string)
obj = json.loads(obj_string)
return self.recompose_base(obj=obj)
def recompose_base(self, obj: dict) -> Base:
@@ -254,7 +252,7 @@ class BaseObjectSerializer:
if not obj:
return
if isinstance(obj, str):
obj = ujson.loads(obj)
obj = json.loads(obj)
if "speckle_type" in obj and obj["speckle_type"] == "reference":
obj = self.get_child(obj=obj)
@@ -268,7 +266,7 @@ class BaseObjectSerializer:
object_type = Base.get_registered_type(speckle_type)
# initialise the base object using `speckle_type` fall back to base if needed
base = object_type() if object_type else Base.of_type(speckle_type=speckle_type)
base = object_type() if object_type else Base(speckle_type=speckle_type)
# get total children count
if "__closure" in obj:
if not self.read_transport:
@@ -292,7 +290,7 @@ class BaseObjectSerializer:
raise SpeckleException(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
ref_obj = ujson.loads(ref_obj_str)
ref_obj = json.loads(ref_obj_str)
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
# 3. handle all other cases (base objects, lists, and dicts)
@@ -326,7 +324,7 @@ class BaseObjectSerializer:
# handle chunked lists
data = []
for o in obj_list:
data.extend(o.data)
data.extend(o["data"])
return data
return obj_list
@@ -350,4 +348,4 @@ class BaseObjectSerializer:
raise SpeckleException(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
return ujson.loads(ref_obj_str)
return json.loads(ref_obj_str)
+5 -15
View File
@@ -22,15 +22,11 @@ class ServerTransport(AbstractTransport):
def __init__(self, client: SpeckleClient, stream_id: str, **data: Any) -> None:
super().__init__(**data)
# TODO: replace client with account or some other auth avenue
if not client.me:
raise SpeckleException("The provided SpeckleClient was not authenticated.")
self.url = client.url
self.stream_id = stream_id
token = client.me["token"]
self._batch_sender = BatchSender(
self.url, self.stream_id, token, max_batch_size_mb=1
)
self._batch_sender = BatchSender(self.url, self.stream_id, token, max_batch_size_mb=1)
self.session = requests.Session()
self.session.headers.update(
@@ -77,25 +73,19 @@ class ServerTransport(AbstractTransport):
r.encoding = "utf-8"
if r.status_code != 200:
raise SpeckleException(
f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})"
)
raise SpeckleException(f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})")
root_obj_serialized = r.text
root_obj = json.loads(root_obj_serialized)
closures = root_obj.get("__closure", {})
closures = root_obj.get('__closure', {})
# Check which children are not already in the target transport
children_ids = list(closures.keys())
children_found_map = target_transport.has_objects(children_ids)
new_children_ids = [
id for id in children_found_map if not children_found_map[id]
]
new_children_ids = [id for id in children_found_map if not children_found_map[id]]
# Get the new children
endpoint = f"{self.url}/api/getobjects/{self.stream_id}"
r = self.session.post(
endpoint, data={"objects": json.dumps(new_children_ids)}, stream=True
)
r = self.session.post(endpoint, data={"objects": json.dumps(new_children_ids)}, stream=True)
if r.encoding is None:
r.encoding = "utf-8"
lines = r.iter_lines(decode_unicode=True)
+27 -30
View File
@@ -6,6 +6,7 @@ import sqlite3
from typing import Any, List, Dict
from appdirs import user_data_dir
from contextlib import closing
from multiprocessing import Process, Queue
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.logging.exceptions import SpeckleException
@@ -18,6 +19,7 @@ class SQLiteTransport(AbstractTransport):
_scheduler = sched.scheduler(time.time, time.sleep)
_polling_interval = 0.5 # seconds
__connection: sqlite3.Connection = None
__queue: Queue = Queue()
app_name: str = ""
scope: str = ""
saved_obj_count: int = 0
@@ -34,18 +36,12 @@ class SQLiteTransport(AbstractTransport):
self.scope = scope or "Objects"
self._base_path = base_path or self.__get_base_path()
try:
os.makedirs(self._base_path, exist_ok=True)
os.makedirs(self._base_path, exist_ok=True)
self._root_path = os.path.join(
os.path.join(self._base_path, f"{self.scope}.db")
)
self.__initialise()
except Exception as ex:
raise SpeckleException(
f"SQLiteTransport could not initialise {self.scope}.db at {self._base_path}. Either provide a different `base_path` or use an alternative transport.",
ex,
)
self._root_path = os.path.join(
os.path.join(self._base_path, f"{self.scope}.db")
)
self.__initialise()
def __repr__(self) -> str:
return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')"
@@ -67,26 +63,26 @@ class SQLiteTransport(AbstractTransport):
if os_name.startswith("Mac"):
system = "darwin"
if system != "darwin":
if system == "darwin":
path = os.path.expanduser("~/.config/")
return os.path.join(path, self.app_name)
else:
return user_data_dir(appname=self.app_name, appauthor=False, roaming=True)
path = os.path.expanduser("~/.config/")
return os.path.join(path, self.app_name)
def __consume_queue(self):
if self._is_writing or self.__queue.empty():
return
print("CONSUME QUEUE")
self._is_writing = True
while not self.__queue.empty():
data = self.__queue.get()
self.save_object(data[0], data[1])
self._is_writing = False
# def __consume_queue(self):
# if self._is_writing or self.__queue.empty():
# return
# print("CONSUME QUEUE")
# self._is_writing = True
# while not self.__queue.empty():
# data = self.__queue.get()
# self.save_object(data[0], data[1])
# self._is_writing = False
# self._scheduler.enter(
# delay=self._polling_interval, priority=1, action=self.__consume_queue
# )
# self._scheduler.run(blocking=True)
self._scheduler.enter(
delay=self._polling_interval, priority=1, action=self.__consume_queue
)
self._scheduler.run(blocking=True)
# def save_object(self, id: str, serialized_object: str) -> None:
# """Adds an object to the queue and schedules it to be saved.
@@ -106,14 +102,15 @@ class SQLiteTransport(AbstractTransport):
def save_object_from_transport(
self, id: str, source_transport: AbstractTransport
) -> None:
"""Adds an object from the given transport to the the local db
"""Adds an object from the given transport to the queue and schedules it to be saved.
Arguments:
id {str} -- the object id
source_transport {AbstractTransport) -- the transport through which the object can be found
"""
serialized_object = source_transport.get_object(id)
self.save_object(id, serialized_object)
self.__queue.put((id, serialized_object))
raise NotImplementedError
def save_object(self, id: str, serialized_object: str) -> None:
"""Directly saves an object into the database.
+1 -2
View File
@@ -90,7 +90,7 @@ def mesh():
[1, 2, 3],
Base(name="detached within a list"),
]
mesh.origin = Point(x=4, y=2)
mesh.origin = Point(value=[4, 2, 0])
return mesh
@@ -102,5 +102,4 @@ def base():
base.vertices = [random.uniform(0, 10) for _ in range(1, 120)]
base.test_bases = [Base(name=i) for i in range(1, 22)]
base["@detach"] = Base(name="detached base")
base["@revit_thing"] = Base.of_type("SpecialRevitFamily", name="secret tho")
return base
-38
View File
@@ -1,9 +1,7 @@
import pytest
from typing import Dict, List
from specklepy.objects import Base
from specklepy.api import operations
from contextlib import ExitStack as does_not_raise
from specklepy.logging.exceptions import SpeckleException
@pytest.mark.parametrize(
@@ -74,39 +72,3 @@ def test_speckle_type_cannot_be_set(base: Base) -> None:
assert base.speckle_type == "Base"
base.speckle_type = "unset"
assert base.speckle_type == "Base"
def test_base_of_custom_speckle_type() -> None:
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
assert b1.speckle_type == "BirdHouse"
assert b1.name == "Tweety's Crib"
class FrozenYoghurt(Base):
"""Testing type checking"""
servings: int
flavours: List[str] = None # list item types won't be checked
customer: str
add_ons: Dict[str, float] # dict item types won't be checked
price: float = 0.0
def test_type_checking() -> None:
order = FrozenYoghurt()
order.servings = 2
order.price = "7" # will get converted
order.customer = "izzy"
with pytest.raises(SpeckleException):
order.flavours = "not a list"
with pytest.raises(SpeckleException):
order.servings = "five"
with pytest.raises(SpeckleException):
order.add_ons = ["sprinkles"]
order.add_ons = {"sprinkles": 0.2, "chocolate": 1.0}
order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
assert order.price == 7.0
+2 -4
View File
@@ -1,4 +1,3 @@
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.objects import Base
from specklepy.transports.memory import MemoryTransport
from specklepy.api.models import Stream
@@ -20,13 +19,12 @@ class TestObject:
return stream
def test_object_create(self, client, stream, base):
transport = SQLiteTransport()
transport = MemoryTransport()
s = BaseObjectSerializer(write_transports=[transport], read_transport=transport)
_, base_dict = s.traverse_base(base)
obj_id = client.object.create(stream_id=stream.id, objects=[base_dict])[0]
assert isinstance(obj_id, str)
assert base_dict["@detach"]["speckle_type"] == "reference"
assert obj_id == base.get_id(True)
def test_object_get(self, client, stream, base):
@@ -37,4 +35,4 @@ class TestObject:
assert isinstance(fetched_base, Base)
assert fetched_base.name == base.name
assert isinstance(fetched_base.vertices, list)
# assert fetched_base["@detach"]["speckle_type"] == "reference"
assert fetched_base["@detach"]["speckle_type"] == "reference"
+1 -2
View File
@@ -19,7 +19,6 @@ class TestSerialization:
assert base.get_id() == deserialized.get_id()
assert base.units == "mm"
assert isinstance(base.test_bases[0], Base)
assert base["@revit_thing"].speckle_type == "SpecialRevitFamily"
assert base["@detach"].name == deserialized["@detach"].name
def test_detaching(self, mesh):
@@ -61,7 +60,7 @@ class TestSerialization:
assert isinstance(received, FakeMesh)
assert received.vertices == mesh.vertices
assert isinstance(received.origin, Point)
assert received.origin.x == mesh.origin.x
assert received.origin.value == mesh.origin.value
# not comparing hashes as order is not guaranteed back from server
mesh.id = hash # populate with decomposed id for use in proceeding tests
-41
View File
@@ -1,41 +0,0 @@
import pytest
from specklepy.api.credentials import StreamWrapper
class TestWrapper:
def test_parse_stream(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
assert wrap.type == "stream"
def test_parse_branch(self):
wacky_wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F"
)
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/next%20level"
)
assert wacky_wrap.type == "branch"
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
assert wrap.type == "branch"
def test_parse_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
)
assert wrap.type == "commit"
def test_parse_object(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6"
)
assert wrap.type == "object"
def test_parse_globals_as_branch(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/0c6ad366c4/globals/")
assert wrap.type == "branch"
def test_parse_globals_as_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893"
)
assert wrap.type == "commit"