Compare commits

...

12 Commits

Author SHA1 Message Date
izzy lyseggen f32196ce1b feat(client/ops): handle invalid token errors (#156)
* feat(client): validate token in `authenticate`

* feat(server transport): catch invalid token

* test(client/ops): error handling of invalid tokens

* feat(client): warning rather than exception
2022-01-21 16:32:33 +00:00
izzylys 6b24e187a5 fix(wrapper): quick acct field fix 2022-01-12 11:33:01 -08:00
izzy lyseggen 129d25df0e Merge pull request #154 from specklesystems/izzy/ujson-fix
fix(serializer): ujson big int issue
2022-01-10 19:34:14 +00:00
izzy lyseggen fa31bd0223 Merge pull request #153 from specklesystems/izzy/streamwrapper-alignment
feat(wrapper): make acct/client private
2022-01-10 19:33:33 +00:00
izzylys 21209b384d feat(wrapper): make acct/client private
closes #139
realised that this doesn't really apply to py since acct is set
automatically by looking for the right server url, but have made this
more explicit by making the acct and client "private"
2022-01-10 11:30:39 -08:00
izzylys 1a9c95871f test(serializer): big int fix 2022-01-10 11:19:11 -08:00
izzylys dc4d583121 fix(serializer): fall back to json lib for big int 2022-01-10 11:19:05 -08:00
izzy lyseggen ed39f0288f Merge pull request #151 from specklesystems/cristi/surface_client_error
surface server validation errors
2022-01-03 16:11:29 +00:00
cristi8 3830706eb1 surface server validation errors 2022-01-02 11:18:56 +02:00
izzy lyseggen f7ae62ade2 Merge pull request #149 from specklesystems/izzy/null-units-hotfix
fix(units): warn and don't set for invalid args
2021-12-22 09:07:40 +00:00
izzy lyseggen 38ffbc27b7 test(units): goddamnit codecov 2021-12-22 09:06:28 +00:00
izzy lyseggen 8cebccf250 fix(units): warn and don't set for invalid args 2021-12-22 09:00:39 +00:00
9 changed files with 126 additions and 33 deletions
+16 -4
View File
@@ -1,6 +1,10 @@
import re
from gql.client import SyncClientSession
from specklepy.logging.exceptions import SpeckleException
from warnings import warn
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
SpeckleWarning,
)
from typing import Dict
from specklepy.api import resources
@@ -14,9 +18,8 @@ from specklepy.api.resources import (
subscriptions,
)
from specklepy.api.models import ServerInfo
from gql import Client, gql
from gql import Client
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.aiohttp import AIOHTTPTransport
from gql.transport.websockets import WebsocketsTransport
@@ -77,6 +80,8 @@ class SpeckleClient:
# Check compatibility with the server
try:
serverInfo = self.server.get()
if isinstance(serverInfo, Exception):
raise serverInfo
if not isinstance(serverInfo, ServerInfo):
raise Exception("Couldn't get ServerInfo")
except Exception as ex:
@@ -111,6 +116,13 @@ class SpeckleClient:
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:
return self.httpclient.execute(query)
+17 -17
View File
@@ -118,8 +118,8 @@ class StreamWrapper:
commit_id: str = None
object_id: str = None
branch_name: str = None
client: SpeckleClient = None
account: Account = None
_client: SpeckleClient = None
_account: Account = None
def __repr__(self):
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)
"""
if self.account:
return self.account
if 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),
None,
)
return self.account
return self._account
def get_client(self, token: str = None) -> SpeckleClient:
"""
@@ -200,22 +200,22 @@ class StreamWrapper:
Returns:
SpeckleClient -- authenticated with a corresponding local account or the provided token
"""
if self.client and token is None:
return self.client
if self._client and token is None:
return self._client
if not self.account:
if not self._account:
self.get_account()
if not self.client:
self.client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if not self._client:
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)
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:
"""
@@ -225,6 +225,6 @@ class StreamWrapper:
Returns:
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)
return ServerTransport(self.stream_id, self.client)
return ServerTransport(self.stream_id, self._client)
+3 -1
View File
@@ -313,7 +313,9 @@ class Base(_RegisteringBase):
@units.setter
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]:
"""Get all of the property names on this object, dynamic or not"""
+8 -1
View File
@@ -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"]
@@ -28,6 +29,12 @@ UNITS_ENCODINGS = {
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)
for name, alternates in UNITS_STRINGS.items():
if unit in alternates:
@@ -1,11 +1,15 @@
import ujson
import hashlib
import re
from uuid import uuid4
from warnings import warn
from typing import Any, Dict, List, Tuple
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
import specklepy.objects.geometry
import specklepy.objects.other
@@ -17,6 +21,19 @@ def hash_obj(obj: Any) -> str:
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:
read_transport: AbstractTransport
write_transports: List[AbstractTransport]
@@ -243,7 +260,7 @@ class BaseObjectSerializer:
"""
if not obj_string:
return None
obj = ujson.loads(obj_string)
obj = safe_json_loads(obj_string)
return self.recompose_base(obj=obj)
def recompose_base(self, obj: dict) -> Base:
@@ -259,7 +276,7 @@ class BaseObjectSerializer:
if not obj:
return
if isinstance(obj, str):
obj = ujson.loads(obj)
obj = safe_json_loads(obj)
if "speckle_type" in obj and obj["speckle_type"] == "reference":
obj = self.get_child(obj=obj)
@@ -297,7 +314,7 @@ class BaseObjectSerializer:
raise SpeckleException(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
ref_obj = ujson.loads(ref_obj_str)
ref_obj = safe_json_loads(ref_obj_str, ref_hash)
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
# 3. handle all other cases (base objects, lists, and dicts)
@@ -355,4 +372,5 @@ class BaseObjectSerializer:
raise SpeckleException(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
return ujson.loads(ref_obj_str)
return safe_json_loads(ref_obj_str, ref_hash)
+9 -4
View File
@@ -92,10 +92,15 @@ class BatchSender(object):
def _bg_send_batch(self, session, batch):
object_ids = [obj[0] for obj in batch]
server_has_object = session.post(
url=f"{self.server_url}/api/diff/{self.stream_id}",
data={"objects": json.dumps(object_ids)},
).json()
try:
server_has_object = session.post(
url=f"{self.server_url}/api/diff/{self.stream_id}",
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 = set(new_object_ids)
+12
View File
@@ -77,6 +77,18 @@ def test_speckle_type_cannot_be_set(base: Base) -> None:
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:
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
assert b1.speckle_type == "BirdHouse"
+31
View File
@@ -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)
+6
View File
@@ -89,3 +89,9 @@ class TestSerialization:
deserialised = operations.deserialize(untyped)
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}