Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f32196ce1b | |||
| 6b24e187a5 | |||
| 129d25df0e | |||
| fa31bd0223 | |||
| 21209b384d | |||
| 1a9c95871f | |||
| dc4d583121 | |||
| ed39f0288f | |||
| 3830706eb1 | |||
| f7ae62ade2 | |||
| 38ffbc27b7 | |||
| 8cebccf250 | |||
| 17aac0b552 | |||
| c281a329a4 | |||
| ca472716db |
+16
-4
@@ -1,6 +1,10 @@
|
|||||||
import re
|
import re
|
||||||
from gql.client import SyncClientSession
|
from warnings import warn
|
||||||
from specklepy.logging.exceptions import SpeckleException
|
from specklepy.logging.exceptions import (
|
||||||
|
GraphQLException,
|
||||||
|
SpeckleException,
|
||||||
|
SpeckleWarning,
|
||||||
|
)
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from specklepy.api import resources
|
from specklepy.api import resources
|
||||||
@@ -14,9 +18,8 @@ from specklepy.api.resources import (
|
|||||||
subscriptions,
|
subscriptions,
|
||||||
)
|
)
|
||||||
from specklepy.api.models import ServerInfo
|
from specklepy.api.models import ServerInfo
|
||||||
from gql import Client, gql
|
from gql import Client
|
||||||
from gql.transport.requests import RequestsHTTPTransport
|
from gql.transport.requests import RequestsHTTPTransport
|
||||||
from gql.transport.aiohttp import AIOHTTPTransport
|
|
||||||
from gql.transport.websockets import WebsocketsTransport
|
from gql.transport.websockets import WebsocketsTransport
|
||||||
|
|
||||||
|
|
||||||
@@ -77,6 +80,8 @@ class SpeckleClient:
|
|||||||
# Check compatibility with the server
|
# Check compatibility with the server
|
||||||
try:
|
try:
|
||||||
serverInfo = self.server.get()
|
serverInfo = self.server.get()
|
||||||
|
if isinstance(serverInfo, Exception):
|
||||||
|
raise serverInfo
|
||||||
if not isinstance(serverInfo, ServerInfo):
|
if not isinstance(serverInfo, ServerInfo):
|
||||||
raise Exception("Couldn't get ServerInfo")
|
raise Exception("Couldn't get ServerInfo")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -111,6 +116,13 @@ class SpeckleClient:
|
|||||||
|
|
||||||
self._init_resources()
|
self._init_resources()
|
||||||
|
|
||||||
|
if isinstance(self.user.get(), GraphQLException):
|
||||||
|
warn(
|
||||||
|
SpeckleWarning(
|
||||||
|
f"Invalid token - could not authenticate Speckle Client for server {self.url}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def execute_query(self, query: str) -> Dict:
|
def execute_query(self, query: str) -> Dict:
|
||||||
return self.httpclient.execute(query)
|
return self.httpclient.execute(query)
|
||||||
|
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ class StreamWrapper:
|
|||||||
commit_id: str = None
|
commit_id: str = None
|
||||||
object_id: str = None
|
object_id: str = None
|
||||||
branch_name: str = None
|
branch_name: str = None
|
||||||
client: SpeckleClient = None
|
_client: SpeckleClient = None
|
||||||
account: Account = None
|
_account: Account = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
|
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
|
||||||
@@ -179,15 +179,15 @@ class StreamWrapper:
|
|||||||
"""
|
"""
|
||||||
Gets an account object for this server from the local accounts db (added via Speckle Manager or a json file)
|
Gets an account object for this server from the local accounts db (added via Speckle Manager or a json file)
|
||||||
"""
|
"""
|
||||||
if self.account:
|
if self._account:
|
||||||
return self.account
|
return self._account
|
||||||
|
|
||||||
self.account = next(
|
self._account = next(
|
||||||
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
|
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.account
|
return self._account
|
||||||
|
|
||||||
def get_client(self, token: str = None) -> SpeckleClient:
|
def get_client(self, token: str = None) -> SpeckleClient:
|
||||||
"""
|
"""
|
||||||
@@ -200,22 +200,22 @@ class StreamWrapper:
|
|||||||
Returns:
|
Returns:
|
||||||
SpeckleClient -- authenticated with a corresponding local account or the provided token
|
SpeckleClient -- authenticated with a corresponding local account or the provided token
|
||||||
"""
|
"""
|
||||||
if self.client and token is None:
|
if self._client and token is None:
|
||||||
return self.client
|
return self._client
|
||||||
|
|
||||||
if not self.account:
|
if not self._account:
|
||||||
self.get_account()
|
self.get_account()
|
||||||
|
|
||||||
if not self.client:
|
if not self._client:
|
||||||
self.client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
|
self._client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
|
||||||
|
|
||||||
if self.account is None and token is None:
|
if self._account is None and token is None:
|
||||||
warn(f"No local account found for server {self.host}", SpeckleWarning)
|
warn(f"No local account found for server {self.host}", SpeckleWarning)
|
||||||
return self.client
|
return self._client
|
||||||
|
|
||||||
self.client.authenticate(self.account.token if self.account else token)
|
self._client.authenticate(self._account.token if self._account else token)
|
||||||
|
|
||||||
return self.client
|
return self._client
|
||||||
|
|
||||||
def get_transport(self, token: str = None) -> ServerTransport:
|
def get_transport(self, token: str = None) -> ServerTransport:
|
||||||
"""
|
"""
|
||||||
@@ -225,6 +225,6 @@ class StreamWrapper:
|
|||||||
Returns:
|
Returns:
|
||||||
ServerTransport -- constructed for this stream with a pre-authenticated client
|
ServerTransport -- constructed for this stream with a pre-authenticated client
|
||||||
"""
|
"""
|
||||||
if not self.client or not self.client.me:
|
if not self._client or not self._client.me:
|
||||||
self.get_client(token)
|
self.get_client(token)
|
||||||
return ServerTransport(self.stream_id, self.client)
|
return ServerTransport(self.stream_id, self._client)
|
||||||
|
|||||||
@@ -252,6 +252,9 @@ class Base(_RegisteringBase):
|
|||||||
if t is None:
|
if t is None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
if t.__module__ == "typing":
|
if t.__module__ == "typing":
|
||||||
origin = getattr(t, "__origin__")
|
origin = getattr(t, "__origin__")
|
||||||
t = (
|
t = (
|
||||||
@@ -310,7 +313,9 @@ class Base(_RegisteringBase):
|
|||||||
|
|
||||||
@units.setter
|
@units.setter
|
||||||
def units(self, value: str):
|
def units(self, value: str):
|
||||||
self._units = get_units_from_string(value)
|
units = get_units_from_string(value)
|
||||||
|
if units:
|
||||||
|
self._units = units
|
||||||
|
|
||||||
def get_member_names(self) -> List[str]:
|
def get_member_names(self) -> List[str]:
|
||||||
"""Get all of the property names on this object, dynamic or not"""
|
"""Get all of the property names on this object, dynamic or not"""
|
||||||
|
|||||||
@@ -391,15 +391,6 @@ class Mesh(
|
|||||||
area: float = None
|
area: float = None
|
||||||
volume: float = None
|
volume: float = None
|
||||||
|
|
||||||
def transform_to(self, transform: "Transform") -> "Mesh":
|
|
||||||
mesh = Mesh(vertices=transform.apply_to_points_values(self.vertices))
|
|
||||||
for attr in set(self.get_serializable_attributes()) - {"vertices"}:
|
|
||||||
orig_val = getattr(self, attr, None)
|
|
||||||
if orig_val:
|
|
||||||
setattr(mesh, attr, orig_val)
|
|
||||||
|
|
||||||
return mesh
|
|
||||||
|
|
||||||
|
|
||||||
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
|
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
|
||||||
degreeU: int = None
|
degreeU: int = None
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from specklepy.logging.exceptions import SpeckleException
|
from warnings import warn
|
||||||
|
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
||||||
|
|
||||||
UNITS = ["mm", "cm", "m", "in", "ft", "yd", "mi"]
|
UNITS = ["mm", "cm", "m", "in", "ft", "yd", "mi"]
|
||||||
|
|
||||||
@@ -28,6 +29,12 @@ UNITS_ENCODINGS = {
|
|||||||
|
|
||||||
|
|
||||||
def get_units_from_string(unit: str):
|
def get_units_from_string(unit: str):
|
||||||
|
if not isinstance(unit, str):
|
||||||
|
warn(
|
||||||
|
f"Invalid units: expected type str but received {type(unit)} ({unit}). Skipping - no units will be set.",
|
||||||
|
SpeckleWarning,
|
||||||
|
)
|
||||||
|
return
|
||||||
unit = str.lower(unit)
|
unit = str.lower(unit)
|
||||||
for name, alternates in UNITS_STRINGS.items():
|
for name, alternates in UNITS_STRINGS.items():
|
||||||
if unit in alternates:
|
if unit in alternates:
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import ujson
|
import ujson
|
||||||
import hashlib
|
import hashlib
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
from warnings import warn
|
||||||
from typing import Any, Dict, List, Tuple
|
from typing import Any, Dict, List, Tuple
|
||||||
from specklepy.objects.base import Base, DataChunk
|
from specklepy.objects.base import Base, DataChunk
|
||||||
from specklepy.logging.exceptions import SerializationException, SpeckleException
|
from specklepy.logging.exceptions import (
|
||||||
|
SerializationException,
|
||||||
|
SpeckleException,
|
||||||
|
SpeckleWarning,
|
||||||
|
)
|
||||||
from specklepy.transports.abstract_transport import AbstractTransport
|
from specklepy.transports.abstract_transport import AbstractTransport
|
||||||
import specklepy.objects.geometry
|
import specklepy.objects.geometry
|
||||||
import specklepy.objects.other
|
import specklepy.objects.other
|
||||||
@@ -17,6 +21,19 @@ def hash_obj(obj: Any) -> str:
|
|||||||
return hashlib.sha256(ujson.dumps(obj).encode()).hexdigest()[:32]
|
return hashlib.sha256(ujson.dumps(obj).encode()).hexdigest()[:32]
|
||||||
|
|
||||||
|
|
||||||
|
def safe_json_loads(obj: str, obj_id=None) -> Any:
|
||||||
|
try:
|
||||||
|
return ujson.loads(obj)
|
||||||
|
except ValueError as err:
|
||||||
|
import json
|
||||||
|
|
||||||
|
warn(
|
||||||
|
f"Failed to deserialise object (id: {obj_id}). This is likely a ujson big int error - falling back to json. \nError: {err}",
|
||||||
|
SpeckleWarning,
|
||||||
|
)
|
||||||
|
return json.loads(obj)
|
||||||
|
|
||||||
|
|
||||||
class BaseObjectSerializer:
|
class BaseObjectSerializer:
|
||||||
read_transport: AbstractTransport
|
read_transport: AbstractTransport
|
||||||
write_transports: List[AbstractTransport]
|
write_transports: List[AbstractTransport]
|
||||||
@@ -60,14 +77,19 @@ class BaseObjectSerializer:
|
|||||||
chunkable = False
|
chunkable = False
|
||||||
detach = False
|
detach = False
|
||||||
|
|
||||||
# skip nulls or props marked to be ignored with "__" or "_"
|
# skip props marked to be ignored with "__" or "_"
|
||||||
if value is None or prop.startswith(("__", "_")):
|
if prop.startswith(("__", "_")):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# don't prepopulate id as this will mess up hashing
|
# don't prepopulate id as this will mess up hashing
|
||||||
if prop == "id":
|
if prop == "id":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# allow serialisation of nulls
|
||||||
|
if value is None:
|
||||||
|
object_builder[prop] = value
|
||||||
|
continue
|
||||||
|
|
||||||
# only bother with chunking and detaching if there is a write transport
|
# only bother with chunking and detaching if there is a write transport
|
||||||
if self.write_transports:
|
if self.write_transports:
|
||||||
dynamic_chunk_match = prop.startswith("@") and re.match(
|
dynamic_chunk_match = prop.startswith("@") and re.match(
|
||||||
@@ -238,7 +260,7 @@ class BaseObjectSerializer:
|
|||||||
"""
|
"""
|
||||||
if not obj_string:
|
if not obj_string:
|
||||||
return None
|
return None
|
||||||
obj = ujson.loads(obj_string)
|
obj = safe_json_loads(obj_string)
|
||||||
return self.recompose_base(obj=obj)
|
return self.recompose_base(obj=obj)
|
||||||
|
|
||||||
def recompose_base(self, obj: dict) -> Base:
|
def recompose_base(self, obj: dict) -> Base:
|
||||||
@@ -254,7 +276,7 @@ class BaseObjectSerializer:
|
|||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
if isinstance(obj, str):
|
if isinstance(obj, str):
|
||||||
obj = ujson.loads(obj)
|
obj = safe_json_loads(obj)
|
||||||
|
|
||||||
if "speckle_type" in obj and obj["speckle_type"] == "reference":
|
if "speckle_type" in obj and obj["speckle_type"] == "reference":
|
||||||
obj = self.get_child(obj=obj)
|
obj = self.get_child(obj=obj)
|
||||||
@@ -292,7 +314,7 @@ class BaseObjectSerializer:
|
|||||||
raise SpeckleException(
|
raise SpeckleException(
|
||||||
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
|
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
|
||||||
)
|
)
|
||||||
ref_obj = ujson.loads(ref_obj_str)
|
ref_obj = safe_json_loads(ref_obj_str, ref_hash)
|
||||||
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
|
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
|
||||||
|
|
||||||
# 3. handle all other cases (base objects, lists, and dicts)
|
# 3. handle all other cases (base objects, lists, and dicts)
|
||||||
@@ -350,4 +372,5 @@ class BaseObjectSerializer:
|
|||||||
raise SpeckleException(
|
raise SpeckleException(
|
||||||
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
|
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
|
||||||
)
|
)
|
||||||
return ujson.loads(ref_obj_str)
|
|
||||||
|
return safe_json_loads(ref_obj_str, ref_hash)
|
||||||
|
|||||||
@@ -92,10 +92,15 @@ class BatchSender(object):
|
|||||||
|
|
||||||
def _bg_send_batch(self, session, batch):
|
def _bg_send_batch(self, session, batch):
|
||||||
object_ids = [obj[0] for obj in batch]
|
object_ids = [obj[0] for obj in batch]
|
||||||
server_has_object = session.post(
|
try:
|
||||||
url=f"{self.server_url}/api/diff/{self.stream_id}",
|
server_has_object = session.post(
|
||||||
data={"objects": json.dumps(object_ids)},
|
url=f"{self.server_url}/api/diff/{self.stream_id}",
|
||||||
).json()
|
data={"objects": json.dumps(object_ids)},
|
||||||
|
).json()
|
||||||
|
except Exception as ex:
|
||||||
|
raise SpeckleException(
|
||||||
|
f"Invalid credentials - cannot send objects to server {self.server_url}"
|
||||||
|
) from ex
|
||||||
|
|
||||||
new_object_ids = [x for x in object_ids if not server_has_object[x]]
|
new_object_ids = [x for x in object_ids if not server_has_object[x]]
|
||||||
new_object_ids = set(new_object_ids)
|
new_object_ids = set(new_object_ids)
|
||||||
|
|||||||
@@ -77,6 +77,18 @@ def test_speckle_type_cannot_be_set(base: Base) -> None:
|
|||||||
assert base.speckle_type == "Base"
|
assert base.speckle_type == "Base"
|
||||||
|
|
||||||
|
|
||||||
|
def test_setting_units():
|
||||||
|
b = Base(units="foot")
|
||||||
|
assert b.units == "ft"
|
||||||
|
|
||||||
|
with pytest.raises(SpeckleException):
|
||||||
|
b.units = "big"
|
||||||
|
|
||||||
|
b.units = None # invalid args are skipped
|
||||||
|
b.units = 7
|
||||||
|
assert b.units == "ft"
|
||||||
|
|
||||||
|
|
||||||
def test_base_of_custom_speckle_type() -> None:
|
def test_base_of_custom_speckle_type() -> None:
|
||||||
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
|
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
|
||||||
assert b1.speckle_type == "BirdHouse"
|
assert b1.speckle_type == "BirdHouse"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import pytest
|
||||||
|
from specklepy.api import operations
|
||||||
|
from specklepy.api.client import SpeckleClient
|
||||||
|
from specklepy.objects.base import Base
|
||||||
|
from specklepy.transports.server import ServerTransport
|
||||||
|
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_authentication():
|
||||||
|
client = SpeckleClient()
|
||||||
|
|
||||||
|
with pytest.warns(SpeckleWarning):
|
||||||
|
client.authenticate("fake token")
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_send():
|
||||||
|
client = SpeckleClient()
|
||||||
|
client.me = {"token": "fake token"}
|
||||||
|
transport = ServerTransport("3073b96e86", client)
|
||||||
|
|
||||||
|
with pytest.raises(SpeckleException):
|
||||||
|
operations.send(Base(), [transport])
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_receive():
|
||||||
|
client = SpeckleClient()
|
||||||
|
client.me = {"token": "fake token"}
|
||||||
|
transport = ServerTransport("fake stream", client)
|
||||||
|
|
||||||
|
with pytest.raises(SpeckleException):
|
||||||
|
operations.receive("fake object", transport)
|
||||||
@@ -89,3 +89,9 @@ class TestSerialization:
|
|||||||
deserialised = operations.deserialize(untyped)
|
deserialised = operations.deserialize(untyped)
|
||||||
|
|
||||||
assert deserialised == {"foo": "bar"}
|
assert deserialised == {"foo": "bar"}
|
||||||
|
|
||||||
|
def test_big_int(self):
|
||||||
|
big_int = '{"big": ' + str(2 ** 64) + "}"
|
||||||
|
deserialised = operations.deserialize(big_int)
|
||||||
|
|
||||||
|
assert deserialised == {"big": 2 ** 64}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
import pytest
|
import pytest
|
||||||
from specklepy.api import operations
|
from specklepy.api import operations
|
||||||
from specklepy.objects.geometry import Mesh, Point, Vector
|
from specklepy.objects.geometry import Point, Vector
|
||||||
from specklepy.objects.other import (
|
from specklepy.objects.other import (
|
||||||
Transform,
|
Transform,
|
||||||
BlockInstance,
|
BlockInstance,
|
||||||
@@ -43,15 +43,6 @@ def vector_value():
|
|||||||
return [1, 1, 2]
|
return [1, 1, 2]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
|
||||||
def mesh():
|
|
||||||
return Mesh(
|
|
||||||
vertices=[-7, 5, 1, -8, 4, 0, -7, 3, 0, -6, 4, 0],
|
|
||||||
faces=[1, 1, 2, 3, 0],
|
|
||||||
units="feet",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def transform():
|
def transform():
|
||||||
"""Translates to [1, 2, 0] and scales z by 0.5"""
|
"""Translates to [1, 2, 0] and scales z by 0.5"""
|
||||||
@@ -138,14 +129,4 @@ def test_transform_serialisation(transform: Transform):
|
|||||||
serialized = operations.serialize(transform)
|
serialized = operations.serialize(transform)
|
||||||
deserialized = operations.deserialize(serialized)
|
deserialized = operations.deserialize(serialized)
|
||||||
|
|
||||||
assert transform.get_id() == deserialized.get_id()
|
assert transform.get_id() == deserialized.get_id()
|
||||||
|
|
||||||
|
|
||||||
def test_mesh_transform(mesh: Mesh, transform: Transform):
|
|
||||||
new_mesh = mesh.transform_to(transform)
|
|
||||||
|
|
||||||
assert mesh.vertices != new_mesh.vertices
|
|
||||||
|
|
||||||
new_mesh.vertices = mesh.vertices
|
|
||||||
|
|
||||||
assert mesh.get_id() == new_mesh.get_id()
|
|
||||||
Reference in New Issue
Block a user