Compare commits

..

37 Commits

Author SHA1 Message Date
izzy lyseggen 9303af6827 Merge pull request #118 from specklesystems/izzy/simpler-typing
feat(base): remove pydantic and roll our own type checking
2021-09-02 12:46:18 +01:00
izzy lyseggen 973dc07d5b fix(base): little tweaks 2021-08-24 15:50:08 +01:00
izzy lyseggen 7dd5b7a2a1 test(base): type checks 2021-08-24 12:11:46 +01:00
izzy lyseggen f259f256c7 feat(base): smol get_member fixes and others 2021-08-24 11:50:23 +01:00
izzy lyseggen 08986056a3 test(obj): quick fix 2021-08-20 13:29:21 +01:00
izzy lyseggen f89b07eacb feat(🥣): custom base types and test fixes 2021-08-20 13:09:09 +01:00
izzy lyseggen c973d916b3 fix(base): py 3.6 typing fix 2021-08-20 12:10:52 +01:00
izzy lyseggen 4ff6288317 feat(🥣): faster get_member_names 2021-08-20 11:56:04 +01:00
izzy lyseggen 8566674f2e Merge pull request #119 from specklesystems/izzy/cred-fix
fix(credentials): fix in get client
2021-08-19 18:28:09 +01:00
izzy lyseggen 1f3b6da9c7 fix(credentials): fix in get client
oopsie think this was a merge error
2021-08-19 18:26:44 +01:00
izzy lyseggen 5d99d5fcad feat(serialisation): swap out json for ujson 2021-08-19 18:25:12 +01:00
izzy lyseggen 4fc07f33d0 fix(base): try fix for 3.7 and 3.6 2021-08-19 17:52:22 +01:00
izzy lyseggen 4e23a69b89 fix(base): chunk fixes 2021-08-19 17:44:50 +01:00
izzy lyseggen 04a0ddc8c4 fix(api): obj receive change w new base 2021-08-19 17:33:41 +01:00
izzy lyseggen 1b4d43e0aa fix(base): add init w kwargs 2021-08-19 16:01:00 +01:00
izzy lyseggen f78c8c407f docs(base): docstring for of_type 2021-08-19 15:50:45 +01:00
izzy lyseggen 892c11f38f fix(base): remove unused init 2021-08-19 14:13:43 +01:00
izzy lyseggen 72639bf4bb fix(base): bypass for setting speckle_type on base 2021-08-19 13:58:25 +01:00
izzy lyseggen b2c210abc1 fix(serialiser): pop speckle_type from obj dict 2021-08-19 13:54:23 +01:00
izzy lyseggen 2250e8a897 fix(wrapper): bug in get_client 2021-08-19 13:53:58 +01:00
izzy lyseggen cb07f55551 feat(base): remove pydantic and diy the type check
- also improves performance by moving adding chunkables/detachables to
  the init_subclass hook
- all inits in the geo classes have been removed
- type checking is enforced on setting attributes from the `Base` class
    - unrecognised types are ignored (no type checking)
    - generics are checked for the generic only, not for the args
    - ints and strs are attempted to be parsed as floats,
      but not the other way around
2021-08-19 12:25:03 +01:00
izzy lyseggen d1b3d5e25e Merge pull request #117 from specklesystems/izzy/tiny-fix
fix(objects): init polyline value w empty list
2021-08-12 11:51:34 +01:00
izzy lyseggen 79cca557f5 fix(objects): init polyline value w empty list
soz!
2021-08-12 11:49:41 +01:00
izzy lyseggen 1e6e66a90a Merge pull request #116 from specklesystems/izzy/commit-spec
feat(client): add `branchName` to commit model
2021-08-11 09:24:49 +01:00
izzy lyseggen 09d84cf64a feat(client): add branchName to commit model 2021-08-11 09:21:04 +01:00
izzy lyseggen 3ccb0ae2a8 ci: try diff env tag variable 2021-08-10 15:49:08 +01:00
izzy lyseggen 6028a38355 Merge pull request #115 from specklesystems/ci/tags-and-versions
ci: fix workflows and patch version with git tag
2021-08-10 15:42:42 +01:00
izzy lyseggen 07418cfc9c ci: rename test job 2021-08-10 15:41:03 +01:00
izzy lyseggen 1ada797d81 ci: rename test job 2021-08-10 15:40:32 +01:00
izzy lyseggen 73703f6237 ci: patch version with git tag 2021-08-10 15:38:02 +01:00
izzy lyseggen 7644af22df fix(ci): add tag filter to build job 2021-08-10 15:37:19 +01:00
izzy lyseggen 564e1d4432 Merge pull request #114 from specklesystems/izzy/stream-wrapper-update
feat(wrapper): add get acct helper
2021-08-10 12:34:08 +01:00
izzy lyseggen fc4511ad02 chore: bump version 2021-08-10 12:03:06 +01:00
izzy lyseggen ad710b72da feat(wrapper): add acct helper 2021-08-10 12:02:42 +01:00
izzy lyseggen 041d9f56ce ci: another workflow fix 🙃 2021-08-06 17:13:16 +01:00
izzy lyseggen e1c0b705ad ci: build in deploy fix 2021-08-06 17:10:55 +01:00
izzy lyseggen 7b011b1122 ci: require build in deploy step 2021-08-06 17:06:42 +01:00
17 changed files with 441 additions and 149 deletions
+9 -6
View File
@@ -4,7 +4,7 @@ orbs:
python: circleci/python@1.3.2
jobs:
build:
test:
docker:
- image: "cimg/python:<<parameters.tag>>"
- image: "circleci/node:12"
@@ -45,20 +45,23 @@ jobs:
- 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]+)*/
+31
View File
@@ -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
View File
@@ -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
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.2.6"
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"
+17 -6
View File
@@ -94,6 +94,7 @@ class StreamWrapper:
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} )"
@@ -148,21 +149,31 @@ class StreamWrapper:
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
acct = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
None,
)
if not self.account:
self.get_account()
self.client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if not acct:
if self.account is None:
warn(f"No local account found for server {self.host}", SpeckleWarning)
return self.client
self.client.authenticate(acct.token)
self.client.authenticate(self.account.token)
return self.client
def get_transport(self) -> ServerTransport:
+1
View File
@@ -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
View File
@@ -1,4 +1,3 @@
import json
from typing import List
from specklepy.objects.base import Base
from specklepy.transports.sqlite import SQLiteTransport
+8 -3
View File
@@ -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 -1
View File
@@ -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
+205 -53
View File
@@ -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 = []
+10 -5
View File
@@ -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):
+66 -57
View File
@@ -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)
+2 -1
View File
@@ -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
+38
View File
@@ -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
+4 -2
View File
@@ -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"
+2 -1
View File
@@ -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