Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9303af6827 | |||
| 973dc07d5b | |||
| 7dd5b7a2a1 | |||
| f259f256c7 | |||
| 08986056a3 | |||
| f89b07eacb | |||
| c973d916b3 | |||
| 4ff6288317 | |||
| 8566674f2e | |||
| 1f3b6da9c7 | |||
| 5d99d5fcad | |||
| 4fc07f33d0 | |||
| 4e23a69b89 | |||
| 04a0ddc8c4 | |||
| 1b4d43e0aa | |||
| f78c8c407f | |||
| 892c11f38f | |||
| 72639bf4bb | |||
| b2c210abc1 | |||
| 2250e8a897 | |||
| cb07f55551 | |||
| d1b3d5e25e | |||
| 79cca557f5 | |||
| 1e6e66a90a | |||
| 09d84cf64a | |||
| 3ccb0ae2a8 | |||
| 6028a38355 | |||
| 07418cfc9c | |||
| 1ada797d81 | |||
| 73703f6237 | |||
| 7644af22df | |||
| 564e1d4432 | |||
| fc4511ad02 | |||
| ad710b72da | |||
| 041d9f56ce | |||
| e1c0b705ad | |||
| 7b011b1122 | |||
| 3f09cd9d77 | |||
| 29a361892b | |||
| 2672b40aff | |||
| 35b6911b27 | |||
| a4f0a2cc2b | |||
| 1970890ecc | |||
| 13df5135b8 | |||
| 4e206b5c60 | |||
| e696091555 | |||
| a512dbb4e4 |
+11
-8
@@ -4,7 +4,7 @@ orbs:
|
||||
python: circleci/python@1.3.2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
test:
|
||||
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:5f8cf11cba07ea6a54000243f9cb343b61cbba13"
|
||||
- image: "speckle/speckle-server"
|
||||
command: ["bash", "-c", "/wait && node bin/www"]
|
||||
environment:
|
||||
POSTGRES_URL: "localhost"
|
||||
@@ -38,27 +38,30 @@ jobs:
|
||||
name: upgrade pip
|
||||
- python/install-packages:
|
||||
pkg-manager: poetry
|
||||
- run: poetry run pytest --version
|
||||
- run: poetry run pytest
|
||||
|
||||
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:
|
||||
- build:
|
||||
jobs:
|
||||
- test:
|
||||
matrix:
|
||||
parameters:
|
||||
tag: ["3.6", "3.7", "3.8", "3.9"]
|
||||
|
||||
publish:
|
||||
jobs:
|
||||
filters:
|
||||
tags:
|
||||
only: /.*/
|
||||
- deploy:
|
||||
requires:
|
||||
- test
|
||||
filters:
|
||||
tags:
|
||||
only: /[0-9]+(\.[0-9]+)*/
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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
+32
-1
@@ -371,6 +371,14 @@ 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"
|
||||
@@ -420,7 +428,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 = "84e846c1bb02924ceada07406e95032e0632d229a36657ba2b85129e68f1526d"
|
||||
content-hash = "728db0014dfb8a83c50fe5ce6e86d068c4c87d319d50fb1e8135e63507713f30"
|
||||
|
||||
[metadata.files]
|
||||
aiohttp = [
|
||||
@@ -711,6 +719,29 @@ 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"},
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "specklepy"
|
||||
version = "2.2.5"
|
||||
version = "2.1.0"
|
||||
description = "The Python SDK for Speckle 2.0"
|
||||
readme = "README.md"
|
||||
authors = ["Speckle Systems <devops@speckle.systems>"]
|
||||
@@ -15,6 +15,7 @@ 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"
|
||||
|
||||
@@ -82,6 +82,11 @@ 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
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import os
|
||||
from typing import List, Optional
|
||||
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 specklepy.api.models import ServerInfo
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.transports.sqlite import SQLiteTransport
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
@@ -79,3 +83,100 @@ 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)
|
||||
@@ -22,6 +22,7 @@ class Commit(BaseModel):
|
||||
authorName: Optional[str]
|
||||
authorId: Optional[str]
|
||||
authorAvatar: Optional[str]
|
||||
branchName: Optional[str]
|
||||
createdAt: Optional[str]
|
||||
sourceApplication: Optional[str]
|
||||
referencedObject: Optional[str]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import json
|
||||
from typing import List
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.transports.sqlite import SQLiteTransport
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from logging import error
|
||||
from specklepy.logging.exceptions import GraphQLException, SpeckleException
|
||||
from specklepy.transports.sqlite import SQLiteTransport
|
||||
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):
|
||||
@@ -40,7 +41,11 @@ class ResourceBase(object):
|
||||
if schema:
|
||||
return schema.parse_obj(response)
|
||||
elif self.schema:
|
||||
return self.schema.parse_obj(response)
|
||||
try:
|
||||
return self.schema.parse_obj(response)
|
||||
except:
|
||||
s = BaseObjectSerializer(read_transport=SQLiteTransport())
|
||||
return s.recompose_base(response)
|
||||
else:
|
||||
return response
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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
|
||||
|
||||
@@ -40,6 +39,7 @@ class Resource(ResourceBase):
|
||||
authorId
|
||||
authorName
|
||||
authorAvatar
|
||||
branchName
|
||||
createdAt
|
||||
sourceApplication
|
||||
totalChildrenCount
|
||||
|
||||
@@ -28,3 +28,8 @@ class GraphQLException(SpeckleException):
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"GraphQLException: {self.message}"
|
||||
|
||||
|
||||
class SpeckleWarning(Warning):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
+205
-53
@@ -1,15 +1,71 @@
|
||||
from inspect import getattr_static
|
||||
from pydantic import BaseModel, validator
|
||||
from pydantic.main import Extra
|
||||
import typing
|
||||
from warnings import warn
|
||||
from typing import get_type_hints
|
||||
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(BaseModel):
|
||||
|
||||
class _RegisteringBase:
|
||||
"""
|
||||
Private Base model for Speckle types.
|
||||
|
||||
@@ -21,7 +77,8 @@ class _RegisteringBase(BaseModel):
|
||||
"""
|
||||
|
||||
speckle_type: ClassVar[str]
|
||||
_type_registry: ClassVar[Dict[str, Type["Base"]]] = {}
|
||||
_type_registry: ClassVar[Dict[str, "Base"]] = {}
|
||||
_attr_types: ClassVar[Dict[str, Type]] = {}
|
||||
|
||||
class Config:
|
||||
validate_assignment = True
|
||||
@@ -33,7 +90,9 @@ class _RegisteringBase(BaseModel):
|
||||
|
||||
def __init_subclass__(
|
||||
cls,
|
||||
speckle_type: Optional[str] = None,
|
||||
speckle_type: str = None,
|
||||
chunkable: Dict[str, int] = None,
|
||||
detachable: Set[str] = None,
|
||||
**kwargs: Dict[str, Any],
|
||||
):
|
||||
"""
|
||||
@@ -51,6 +110,15 @@ class _RegisteringBase(BaseModel):
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
@@ -63,6 +131,11 @@ 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}, "
|
||||
@@ -73,6 +146,23 @@ 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
|
||||
@@ -82,18 +172,41 @@ class Base(_RegisteringBase):
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
"""
|
||||
Guard attribute and property set mechanism.
|
||||
Type checking, 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":
|
||||
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)
|
||||
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}")
|
||||
|
||||
@classmethod
|
||||
def validate_prop_name(cls, name: str) -> None:
|
||||
@@ -109,6 +222,45 @@ 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
|
||||
@@ -136,53 +288,50 @@ 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 = list(self.__dict__.keys())
|
||||
properties = [
|
||||
# attrs = set(self.__dict__.keys())
|
||||
attr_dir = list(set(dir(self)) - REMOVE_FROM_DIR)
|
||||
return [
|
||||
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(type(self), name, None), property)
|
||||
for name in attr_dir
|
||||
if not name.startswith("_") and not callable(getattr(self, name))
|
||||
]
|
||||
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.__fields__.keys())
|
||||
return list(self._attr_types.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.__fields__.keys()))
|
||||
return list(set(self.__dict__.keys()) - set(self._attr_types.keys()))
|
||||
|
||||
def get_children_count(self) -> int:
|
||||
"""Get the total count of children Base objects"""
|
||||
@@ -217,7 +366,7 @@ class Base(_RegisteringBase):
|
||||
|
||||
return sum(
|
||||
self._handle_object_count(value, parsed)
|
||||
for name, value in base.__dict__.items()
|
||||
for name, value in base.get_member_names()
|
||||
if not name.startswith("@")
|
||||
)
|
||||
|
||||
@@ -245,9 +394,12 @@ class Base(_RegisteringBase):
|
||||
count += self._handle_object_count(value, parsed)
|
||||
return count
|
||||
|
||||
class Config:
|
||||
extra = Extra.allow
|
||||
|
||||
Base.update_forward_refs()
|
||||
|
||||
|
||||
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
|
||||
data: List[Any] = []
|
||||
data: List[Any] = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.data = []
|
||||
|
||||
@@ -14,7 +14,12 @@ CHUNKABLE_PROPS = {
|
||||
DETACHABLE = {"detach_this", "origin", "detached_list"}
|
||||
|
||||
|
||||
class FakeMesh(Base):
|
||||
class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
|
||||
pointslist: List[Base] = None
|
||||
dots: List[int] = None
|
||||
|
||||
|
||||
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
|
||||
vertices: List[float] = None
|
||||
faces: List[int] = None
|
||||
colors: List[int] = None
|
||||
@@ -24,10 +29,10 @@ class FakeMesh(Base):
|
||||
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):
|
||||
|
||||
@@ -5,25 +5,27 @@ GEOMETRY = "Objects.Geometry."
|
||||
|
||||
|
||||
class Interval(Base, speckle_type="Objects.Primitive.Interval"):
|
||||
start: float = 0
|
||||
end: float = 0
|
||||
start: float = 0.0
|
||||
end: float = 0.0
|
||||
|
||||
def length(self):
|
||||
return abs(self.start - self.end)
|
||||
|
||||
|
||||
class Point(Base, speckle_type=GEOMETRY + "Point"):
|
||||
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
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
z: float = 0.0
|
||||
|
||||
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
|
||||
|
||||
|
||||
class Vector(Point, speckle_type=GEOMETRY + "Vector"):
|
||||
pass
|
||||
@@ -92,7 +94,7 @@ class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
|
||||
length: float = None
|
||||
|
||||
|
||||
class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
|
||||
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
|
||||
value: List[float] = None
|
||||
closed: bool = None
|
||||
domain: Interval = None
|
||||
@@ -100,14 +102,11 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
|
||||
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
|
||||
@@ -131,10 +130,16 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
|
||||
raise ValueError("Points array malformed: length%3 != 0.")
|
||||
|
||||
values = iter(self.value)
|
||||
return [Point(v, next(values), next(values), units=self.units) for v in values]
|
||||
return [
|
||||
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
|
||||
]
|
||||
|
||||
|
||||
class Curve(Base, speckle_type=GEOMETRY + "Curve"):
|
||||
class Curve(
|
||||
Base,
|
||||
speckle_type=GEOMETRY + "Curve",
|
||||
chunkable={"points": 20000, "weights": 20000, "knots": 20000},
|
||||
):
|
||||
degree: int = None
|
||||
periodic: bool = None
|
||||
rational: bool = None
|
||||
@@ -148,10 +153,6 @@ class Curve(Base, speckle_type=GEOMETRY + "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:
|
||||
@@ -161,11 +162,13 @@ class Curve(Base, speckle_type=GEOMETRY + "Curve"):
|
||||
raise ValueError("Points array malformed: length%3 != 0.")
|
||||
|
||||
values = iter(self.points)
|
||||
return [Point(v, next(values), next(values), units=self.units) for v in values]
|
||||
return [
|
||||
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
|
||||
]
|
||||
|
||||
|
||||
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
|
||||
segments: List[Base] = []
|
||||
segments: List[Base] = None
|
||||
domain: Interval = None
|
||||
closed: bool = None
|
||||
bbox: Box = None
|
||||
@@ -187,7 +190,16 @@ class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
|
||||
bbox: Box = None
|
||||
|
||||
|
||||
class Mesh(Base, speckle_type=GEOMETRY + "Mesh"):
|
||||
class Mesh(
|
||||
Base,
|
||||
speckle_type=GEOMETRY + "Mesh",
|
||||
chunkable={
|
||||
"vertices": 2000,
|
||||
"faces": 2000,
|
||||
"colors": 2000,
|
||||
"textureCoordinates": 2000,
|
||||
},
|
||||
):
|
||||
vertices: List[float] = None
|
||||
faces: List[int] = None
|
||||
colors: List[int] = None
|
||||
@@ -196,12 +208,6 @@ class Mesh(Base, speckle_type=GEOMETRY + "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
|
||||
@@ -231,7 +237,8 @@ class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
|
||||
|
||||
@property
|
||||
def _loops(self):
|
||||
return [self._Brep.Loops[index] for index in self.LoopIndices]
|
||||
if self.LoopIndices:
|
||||
return [self._Brep.Loops[i] for i in self.LoopIndices]
|
||||
|
||||
|
||||
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
|
||||
@@ -253,7 +260,8 @@ class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
|
||||
|
||||
@property
|
||||
def _trims(self):
|
||||
return [self._Brep.Trims[i] for i in self.TrimIndices]
|
||||
if self.TrimIndices:
|
||||
return [self._Brep.Trims[i] for i in self.TrimIndices]
|
||||
|
||||
@property
|
||||
def _curve(self):
|
||||
@@ -272,7 +280,8 @@ class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
|
||||
|
||||
@property
|
||||
def _trims(self):
|
||||
return [self._Brep.Trims[i] for i in self.TrimIndices]
|
||||
if self.TrimIndices:
|
||||
return [self._Brep.Trims[i] for i in self.TrimIndices]
|
||||
|
||||
|
||||
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
|
||||
@@ -305,41 +314,41 @@ class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
|
||||
return self._Brep.Curve2D[self.CurveIndex]
|
||||
|
||||
|
||||
class Brep(Base, speckle_type=GEOMETRY + "Brep"):
|
||||
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"},
|
||||
):
|
||||
provenance: str = None
|
||||
bbox: Box = None
|
||||
area: float = None
|
||||
volume: float = None
|
||||
displayValue: Mesh = 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] = []
|
||||
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
|
||||
IsClosed: bool = 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,
|
||||
)
|
||||
Orientation: int = None
|
||||
|
||||
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,4 +1,4 @@
|
||||
import json
|
||||
import ujson
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
@@ -14,7 +14,7 @@ PRIMITIVES = (int, float, str, bool)
|
||||
|
||||
|
||||
def hash_obj(obj: Any) -> str:
|
||||
return hashlib.sha256(json.dumps(obj).encode()).hexdigest()[:32]
|
||||
return hashlib.sha256(ujson.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, json.dumps(obj)
|
||||
return hash, ujson.dumps(obj)
|
||||
|
||||
def traverse_base(self, base: Base) -> Tuple[str, Dict]:
|
||||
"""Decomposes the given base object and builds a serializable dictionary
|
||||
@@ -70,7 +70,9 @@ class BaseObjectSerializer:
|
||||
|
||||
# only bother with chunking and detaching if there is a write transport
|
||||
if self.write_transports:
|
||||
dynamic_chunk_match = re.match(r"^@\((\d*)\)", prop)
|
||||
dynamic_chunk_match = prop.startswith("@") and re.match(
|
||||
r"^@\((\d*)\)", prop
|
||||
)
|
||||
if dynamic_chunk_match:
|
||||
chunk_size = dynamic_chunk_match.groups()[0]
|
||||
base._chunkable[prop] = (
|
||||
@@ -140,7 +142,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=json.dumps(object_builder))
|
||||
t.save_object(id=hash, serialized_object=ujson.dumps(object_builder))
|
||||
|
||||
del self.lineage[-1]
|
||||
|
||||
@@ -236,7 +238,7 @@ class BaseObjectSerializer:
|
||||
"""
|
||||
if not obj_string:
|
||||
return None
|
||||
obj = json.loads(obj_string)
|
||||
obj = ujson.loads(obj_string)
|
||||
return self.recompose_base(obj=obj)
|
||||
|
||||
def recompose_base(self, obj: dict) -> Base:
|
||||
@@ -252,7 +254,7 @@ class BaseObjectSerializer:
|
||||
if not obj:
|
||||
return
|
||||
if isinstance(obj, str):
|
||||
obj = json.loads(obj)
|
||||
obj = ujson.loads(obj)
|
||||
|
||||
if "speckle_type" in obj and obj["speckle_type"] == "reference":
|
||||
obj = self.get_child(obj=obj)
|
||||
@@ -266,7 +268,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(speckle_type=speckle_type)
|
||||
base = object_type() if object_type else Base.of_type(speckle_type=speckle_type)
|
||||
# get total children count
|
||||
if "__closure" in obj:
|
||||
if not self.read_transport:
|
||||
@@ -290,7 +292,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 = json.loads(ref_obj_str)
|
||||
ref_obj = ujson.loads(ref_obj_str)
|
||||
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
|
||||
|
||||
# 3. handle all other cases (base objects, lists, and dicts)
|
||||
@@ -324,7 +326,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
|
||||
|
||||
@@ -348,4 +350,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 json.loads(ref_obj_str)
|
||||
return ujson.loads(ref_obj_str)
|
||||
|
||||
@@ -22,11 +22,15 @@ 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(
|
||||
@@ -73,19 +77,25 @@ 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)
|
||||
|
||||
+2
-1
@@ -90,7 +90,7 @@ def mesh():
|
||||
[1, 2, 3],
|
||||
Base(name="detached within a list"),
|
||||
]
|
||||
mesh.origin = Point(value=[4, 2, 0])
|
||||
mesh.origin = Point(x=4, y=2)
|
||||
return mesh
|
||||
|
||||
|
||||
@@ -102,4 +102,5 @@ 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
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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(
|
||||
@@ -72,3 +74,39 @@ 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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from specklepy.transports.sqlite import SQLiteTransport
|
||||
from specklepy.objects import Base
|
||||
from specklepy.transports.memory import MemoryTransport
|
||||
from specklepy.api.models import Stream
|
||||
@@ -19,12 +20,13 @@ class TestObject:
|
||||
return stream
|
||||
|
||||
def test_object_create(self, client, stream, base):
|
||||
transport = MemoryTransport()
|
||||
transport = SQLiteTransport()
|
||||
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):
|
||||
@@ -35,4 +37,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"
|
||||
|
||||
@@ -19,6 +19,7 @@ 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):
|
||||
@@ -60,7 +61,7 @@ class TestSerialization:
|
||||
assert isinstance(received, FakeMesh)
|
||||
assert received.vertices == mesh.vertices
|
||||
assert isinstance(received.origin, Point)
|
||||
assert received.origin.value == mesh.origin.value
|
||||
assert received.origin.x == mesh.origin.x
|
||||
# not comparing hashes as order is not guaranteed back from server
|
||||
|
||||
mesh.id = hash # populate with decomposed id for use in proceeding tests
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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"
|
||||
Reference in New Issue
Block a user