Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f7ae62ade2 | |||
| 38ffbc27b7 | |||
| 8cebccf250 | |||
| 17aac0b552 | |||
| c281a329a4 | |||
| ca472716db | |||
| af50afe3ff | |||
| b6493df77f | |||
| 59d3c8c3ea | |||
| 4e3405f1fb | |||
| 49eabdd712 | |||
| 96a31f0678 | |||
| 91506b0b20 | |||
| b0de9e31b5 | |||
| 2075783134 | |||
| 071f2449c3 | |||
| a419664461 | |||
| 4a0c07009b | |||
| 682bcbfa9f | |||
| ccf284e8fa | |||
| 23102a28ff | |||
| 5475edb253 | |||
| c52f80c1ef | |||
| 21eecfa24c | |||
| 5dde1bfcf1 | |||
| 82c9d874c9 | |||
| 9acf2c8a92 | |||
| 95012e60c1 | |||
| 19b6500bbd | |||
| 47a06e4630 | |||
| e5a8b40bb2 | |||
| 219456f5f8 | |||
| d1544ae89f | |||
| 8f7d4b2ca7 | |||
| a7d31d4983 | |||
| a89b12a02c | |||
| 15ae68f5d7 | |||
| 0709cd99b5 | |||
| faf06f7141 | |||
| b54e09f811 | |||
| 55b7e0d732 | |||
| 45c922679b | |||
| b1c149382a | |||
| 393e98c8c2 | |||
| 8376329cbb | |||
| 1567fe9e68 | |||
| 364b826a1b | |||
| 297dbab479 |
+13
-4
@@ -2,14 +2,15 @@ version: 2.1
|
||||
|
||||
orbs:
|
||||
python: circleci/python@1.3.2
|
||||
codecov: codecov/codecov@3.2.2
|
||||
|
||||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: "cimg/python:<<parameters.tag>>"
|
||||
- image: "circleci/node:12"
|
||||
- image: "circleci/redis:6"
|
||||
- image: "circleci/postgres:12"
|
||||
- image: 'cimg/node:14.18'
|
||||
- image: 'circleci/redis:6'
|
||||
- image: 'cimg/postgres:12.8'
|
||||
environment:
|
||||
POSTGRES_DB: speckle2_test
|
||||
POSTGRES_PASSWORD: speckle
|
||||
@@ -38,7 +39,15 @@ jobs:
|
||||
name: upgrade pip
|
||||
- python/install-packages:
|
||||
pkg-manager: poetry
|
||||
- run: poetry run pytest
|
||||
- run: poetry run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
|
||||
|
||||
- store_test_results:
|
||||
path: reports
|
||||
|
||||
- store_artifacts:
|
||||
path: reports
|
||||
|
||||
- codecov/upload
|
||||
|
||||
deploy:
|
||||
docker:
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.tool-versions
|
||||
.envrc
|
||||
reports/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
|
||||
|
||||
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square&logo=discourse&logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white" alt="docs"></a></p>
|
||||
<p align="center"><a href="https://github.com/specklesystems/specklepy/"><img src="https://circleci.com/gh/specklesystems/specklepy.svg?style=svg&circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
|
||||
<p align="center"><a href="https://github.com/specklesystems/specklepy/"><img src="https://circleci.com/gh/specklesystems/specklepy.svg?style=svg&circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a><a href="https://codecov.io/gh/specklesystems/specklepy">
|
||||
<img src="https://codecov.io/gh/specklesystems/specklepy/branch/main/graph/badge.svg?token=8KQFL5N0YF"/>
|
||||
</a> </p>
|
||||
|
||||
# About Speckle
|
||||
|
||||
|
||||
Generated
+531
-281
File diff suppressed because it is too large
Load Diff
+5
-4
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "specklepy"
|
||||
version = "2.1.0"
|
||||
version = "2.4.0"
|
||||
description = "The Python SDK for Speckle 2.0"
|
||||
readme = "README.md"
|
||||
authors = ["Speckle Systems <devops@speckle.systems>"]
|
||||
@@ -12,16 +12,17 @@ homepage = "https://speckle.systems/"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6.5"
|
||||
pydantic = "^1.7.3"
|
||||
pydantic = "^1.8.2"
|
||||
appdirs = "^1.4.4"
|
||||
gql = {version = ">=3.0.0a6", extras = ["all"], allow-prereleases = true}
|
||||
ujson = "^4.1.0"
|
||||
gql = {version = ">=3.0.0b1", extras = ["all"], allow-prereleases = true}
|
||||
ujson = "^4.3.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^20.8b1"
|
||||
isort = "^5.7.0"
|
||||
pytest = "^6.2.2"
|
||||
pytest-ordering = "^0.6"
|
||||
pytest-cov = "^3.0.0"
|
||||
|
||||
|
||||
[tool.black]
|
||||
|
||||
@@ -89,6 +89,28 @@ def get_default_account(base_path: str = None) -> Account:
|
||||
|
||||
|
||||
class StreamWrapper:
|
||||
"""
|
||||
The `StreamWrapper` gives you some handy helpers to deal with urls and get authenticated clients and transports.
|
||||
|
||||
Construct a `StreamWrapper` with a stream, branch, commit, or object URL. The corresponding ids will be stored
|
||||
in the wrapper. If you have local accounts on the machine, you can use the `get_account` and `get_client` methods
|
||||
to get a local account for the server. You can also pass a token into `get_client` if you don't have a corresponding
|
||||
local account for the server.
|
||||
|
||||
```py
|
||||
from specklepy.api.credentials import StreamWrapper
|
||||
|
||||
# provide any stream, branch, commit, object, or globals url
|
||||
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
|
||||
|
||||
# get an authenticated SpeckleClient if you have a local account for the server
|
||||
client = wrapper.get_client()
|
||||
|
||||
# get an authenticated ServerTransport if you have a local account for the server
|
||||
transport = wrapper.get_transport()
|
||||
```
|
||||
"""
|
||||
|
||||
stream_url: str = None
|
||||
use_ssl: bool = True
|
||||
host: str = None
|
||||
@@ -122,9 +144,9 @@ class StreamWrapper:
|
||||
parsed = urlparse(url)
|
||||
self.host = parsed.netloc
|
||||
self.use_ssl = parsed.scheme == "https"
|
||||
segments = parsed.path.strip("/").split("/")
|
||||
segments = parsed.path.strip("/").split("/", 3)
|
||||
|
||||
if not segments or len(segments) > 4 or len(segments) < 2:
|
||||
if not segments or len(segments) < 2:
|
||||
raise SpeckleException(
|
||||
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
|
||||
)
|
||||
@@ -154,6 +176,9 @@ class StreamWrapper:
|
||||
)
|
||||
|
||||
def get_account(self) -> Account:
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -164,23 +189,42 @@ class StreamWrapper:
|
||||
|
||||
return self.account
|
||||
|
||||
def get_client(self) -> SpeckleClient:
|
||||
if self.client:
|
||||
def get_client(self, token: str = None) -> SpeckleClient:
|
||||
"""
|
||||
Gets an authenticated client for this server. You may provide a token if there aren't any local accounts on this
|
||||
machine. If no account is found and no token is provided, an unauthenticated client is returned.
|
||||
|
||||
Arguments:
|
||||
token {str} -- optional token if no local account is available (defaults to None)
|
||||
|
||||
Returns:
|
||||
SpeckleClient -- authenticated with a corresponding local account or the provided token
|
||||
"""
|
||||
if self.client and token is None:
|
||||
return self.client
|
||||
|
||||
if not self.account:
|
||||
self.get_account()
|
||||
|
||||
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:
|
||||
if self.account is None and token is None:
|
||||
warn(f"No local account found for server {self.host}", SpeckleWarning)
|
||||
return self.client
|
||||
|
||||
self.client.authenticate(self.account.token)
|
||||
self.client.authenticate(self.account.token if self.account else token)
|
||||
|
||||
return self.client
|
||||
|
||||
def get_transport(self) -> ServerTransport:
|
||||
if not self.client:
|
||||
self.get_client()
|
||||
return ServerTransport(self.client, self.stream_id)
|
||||
def get_transport(self, token: str = None) -> ServerTransport:
|
||||
"""
|
||||
Gets a server transport for this stream using an authenticated client. If there is no local account for this
|
||||
server and the client was not authenticated with a token, this will throw an exception.
|
||||
|
||||
Returns:
|
||||
ServerTransport -- constructed for this stream with a pre-authenticated client
|
||||
"""
|
||||
if not self.client or not self.client.me:
|
||||
self.get_client(token)
|
||||
return ServerTransport(self.stream_id, self.client)
|
||||
|
||||
@@ -30,7 +30,7 @@ class Commit(BaseModel):
|
||||
parents: Optional[List[str]]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Commit( id: {self.id}, message: {self.message}, referencedObject: {self.referencedObject}, authorName: {self.authorName}, createdAt: {self.createdAt} )"
|
||||
return f"Commit( id: {self.id}, message: {self.message}, referencedObject: {self.referencedObject}, authorName: {self.authorName}, branchName: {self.branchName}, createdAt: {self.createdAt} )"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__repr__()
|
||||
|
||||
@@ -22,7 +22,7 @@ def send(
|
||||
Returns:
|
||||
str -- the object id of the sent object
|
||||
"""
|
||||
metrics.track(metrics.RECEIVE)
|
||||
metrics.track(metrics.SEND)
|
||||
if not transports and not use_default_cache:
|
||||
raise SpeckleException(
|
||||
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
|
||||
|
||||
@@ -34,8 +34,8 @@ class Resource(ResourceBase):
|
||||
stream(id: $stream_id) {
|
||||
commit(id: $commit_id) {
|
||||
id
|
||||
referencedObject
|
||||
message
|
||||
referencedObject
|
||||
authorId
|
||||
authorName
|
||||
authorAvatar
|
||||
@@ -79,6 +79,7 @@ class Resource(ResourceBase):
|
||||
authorId
|
||||
authorName
|
||||
authorAvatar
|
||||
branchName
|
||||
createdAt
|
||||
sourceApplication
|
||||
totalChildrenCount
|
||||
@@ -185,3 +186,40 @@ class Resource(ResourceBase):
|
||||
return self.make_request(
|
||||
query=query, params=params, return_type="commitDelete", parse_response=False
|
||||
)
|
||||
|
||||
def received(
|
||||
self,
|
||||
stream_id: str,
|
||||
commit_id: str,
|
||||
source_application: str = "python",
|
||||
message: Optional[str] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
Mark a commit object a received by the source application.
|
||||
"""
|
||||
query = gql(
|
||||
"""
|
||||
mutation CommitReceive($receivedInput:CommitReceivedInput!){
|
||||
commitReceive(input:$receivedInput)
|
||||
}
|
||||
"""
|
||||
)
|
||||
params = {
|
||||
"receivedInput": {
|
||||
"sourceApplication": source_application,
|
||||
"streamId": stream_id,
|
||||
"commitId": commit_id,
|
||||
"message": "message",
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
return self.make_request(
|
||||
query=query,
|
||||
params=params,
|
||||
return_type="commitReceive",
|
||||
parse_response=False,
|
||||
)
|
||||
except Exception as ex:
|
||||
print(ex.with_traceback)
|
||||
return False
|
||||
|
||||
@@ -32,4 +32,4 @@ class GraphQLException(SpeckleException):
|
||||
|
||||
class SpeckleWarning(Warning):
|
||||
def __init__(self, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
super().__init__(*args)
|
||||
|
||||
@@ -122,4 +122,4 @@ class MetricsTracker(metaclass=Singleton):
|
||||
except Exception as ex:
|
||||
LOG.error("Error sending metrics request: " + str(ex))
|
||||
|
||||
self.queue.task_done()
|
||||
self.queue.task_done()
|
||||
|
||||
+40
-22
@@ -1,6 +1,15 @@
|
||||
import typing
|
||||
from typing import (Any, Callable, ClassVar, Dict, List, Optional, Set, Type,
|
||||
get_type_hints)
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Set,
|
||||
Type,
|
||||
get_type_hints,
|
||||
)
|
||||
from warnings import warn
|
||||
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
@@ -118,14 +127,12 @@ class _RegisteringBase:
|
||||
except Exception:
|
||||
cls._attr_types = getattr(cls, "__annotations__", {})
|
||||
if chunkable:
|
||||
chunkable = {k: v for k, v in chunkable.items()
|
||||
if isinstance(v, int)}
|
||||
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)
|
||||
if serialize_ignore:
|
||||
cls._serialize_ignore = cls._serialize_ignore.union(
|
||||
serialize_ignore)
|
||||
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
|
||||
@@ -200,7 +207,7 @@ class Base(_RegisteringBase):
|
||||
try:
|
||||
attr.__set__(self, value)
|
||||
except AttributeError:
|
||||
pass # the prop probably doesn't have a setter
|
||||
return # the prop probably doesn't have a setter
|
||||
super().__setattr__(name, value)
|
||||
|
||||
@classmethod
|
||||
@@ -215,15 +222,13 @@ class Base(_RegisteringBase):
|
||||
try:
|
||||
cls._attr_types = get_type_hints(cls)
|
||||
except Exception as e:
|
||||
warn(
|
||||
f"Could not update forward refs for class {cls.__name__}: {e}")
|
||||
warn(f"Could not update forward refs for class {cls.__name__}: {e}")
|
||||
|
||||
@classmethod
|
||||
def validate_prop_name(cls, name: str) -> None:
|
||||
"""Validator for dynamic attribute names."""
|
||||
if name in {"", "@"}:
|
||||
raise ValueError(
|
||||
"Invalid Name: Base member names cannot be empty strings")
|
||||
raise ValueError("Invalid Name: Base member names cannot be empty strings")
|
||||
if name.startswith("@@"):
|
||||
raise ValueError(
|
||||
"Invalid Name: Base member names cannot start with more than one '@'",
|
||||
@@ -247,9 +252,17 @@ class Base(_RegisteringBase):
|
||||
if t is None:
|
||||
return value
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if t.__module__ == "typing":
|
||||
origin = getattr(t, "__origin__")
|
||||
t = t.__args__ if origin is typing.Union else origin
|
||||
t = (
|
||||
tuple(getattr(sub_t, "__origin__", sub_t) for sub_t in 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."
|
||||
@@ -260,13 +273,16 @@ class Base(_RegisteringBase):
|
||||
|
||||
# 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:
|
||||
if isinstance(t, tuple):
|
||||
t = t[0]
|
||||
|
||||
try:
|
||||
if t is float:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
pass
|
||||
if t is str and value is not None:
|
||||
return str(value)
|
||||
if t is str and value:
|
||||
return str(value)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
raise SpeckleException(
|
||||
f"Cannot set '{self.__class__.__name__}.{name}': it expects type '{t.__name__}', but received type '{type(value).__name__}'"
|
||||
@@ -297,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"""
|
||||
@@ -327,7 +345,8 @@ class Base(_RegisteringBase):
|
||||
|
||||
def get_id(self, decompose: bool = False) -> str:
|
||||
"""
|
||||
Gets the id (a unique hash) of this object. ⚠️ This method fully serializes the object, which in the case of large objects (with many sub-objects), has a tangible cost. Avoid using it!
|
||||
Gets the id (a unique hash) of this object. ⚠️ This method fully serializes the object which,
|
||||
in the case of large objects (with many sub-objects), has a tangible cost. Avoid using it!
|
||||
|
||||
Note: the hash of a decomposed object differs from that of a non-decomposed object
|
||||
|
||||
@@ -337,8 +356,7 @@ class Base(_RegisteringBase):
|
||||
Returns:
|
||||
str -- the hash (id) of the fully serialized object
|
||||
"""
|
||||
from specklepy.serialization.base_object_serializer import \
|
||||
BaseObjectSerializer
|
||||
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
|
||||
|
||||
serializer = BaseObjectSerializer()
|
||||
if decompose:
|
||||
|
||||
@@ -17,6 +17,7 @@ class CurveTypeEncoding(int, Enum):
|
||||
@property
|
||||
def object_class(self) -> Type:
|
||||
from . import geometry
|
||||
|
||||
if self == self.Arc:
|
||||
return geometry.Arc
|
||||
elif self == self.Circle:
|
||||
@@ -32,7 +33,8 @@ class CurveTypeEncoding(int, Enum):
|
||||
elif self == self.Polycurve:
|
||||
return geometry.Polycurve
|
||||
raise SpeckleException(
|
||||
f'No corresponding object class for CurveTypeEncoding: {self}')
|
||||
f"No corresponding object class for CurveTypeEncoding: {self}"
|
||||
)
|
||||
|
||||
|
||||
def curve_from_list(args: List[float]):
|
||||
@@ -41,61 +43,68 @@ def curve_from_list(args: List[float]):
|
||||
|
||||
|
||||
class ObjectArray:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.data = []
|
||||
|
||||
@classmethod
|
||||
def from_objects(cls, objects: List[Base]) -> 'ObjectArray':
|
||||
data_chunk = cls()
|
||||
if len(objects) == 0:
|
||||
return data_chunk
|
||||
def from_objects(cls, objects: List[Base]) -> "ObjectArray":
|
||||
data_list = cls()
|
||||
if not objects:
|
||||
return data_list
|
||||
|
||||
speckle_type = objects[0].speckle_type
|
||||
|
||||
for obj in objects:
|
||||
if speckle_type != obj.speckle_type:
|
||||
raise SpeckleException(
|
||||
'All objects in chunk should have the same speckle_type. '
|
||||
f'Found {speckle_type} and {obj.speckle_type}'
|
||||
"All objects in chunk should have the same speckle_type. "
|
||||
f"Found {speckle_type} and {obj.speckle_type}"
|
||||
)
|
||||
data_chunk.encode_object(object=obj)
|
||||
data_list.encode_object(object=obj)
|
||||
|
||||
return data_chunk
|
||||
return data_list
|
||||
|
||||
@staticmethod
|
||||
def decode_data(data: List[Any], decoder: Callable[[List[Any]], Base]) -> List[Base]:
|
||||
def decode_data(
|
||||
data: List[Any], decoder: Callable[[List[Any]], Base]
|
||||
) -> List[Base]:
|
||||
bases = []
|
||||
if not data:
|
||||
return bases
|
||||
|
||||
index = 0
|
||||
unchunked_data = []
|
||||
while index < len(data):
|
||||
chunk_length = data[index]
|
||||
chunk_start = int(index + 1)
|
||||
chunk_end = int(chunk_start + chunk_length)
|
||||
chunk_data = data[chunk_start:chunk_end]
|
||||
decoded_data = decoder(chunk_data)
|
||||
unchunked_data.append(decoded_data)
|
||||
index = chunk_end
|
||||
return unchunked_data
|
||||
item_length = data[index]
|
||||
item_start = index + 1
|
||||
item_end = item_start + item_length
|
||||
item_data = data[item_start:item_end]
|
||||
index = item_end
|
||||
# TODO: investigate what's going on w this fail
|
||||
try:
|
||||
decoded_data = decoder(item_data)
|
||||
bases.append(decoded_data)
|
||||
except ValueError:
|
||||
continue
|
||||
return bases
|
||||
|
||||
def decode(self, decoder: Callable[[List[Any]], Any]):
|
||||
return self.decode_data(data=self.data, decoder=decoder)
|
||||
|
||||
def encode_object(self, object: Base):
|
||||
chunk = object.to_list()
|
||||
chunk.insert(0, len(chunk))
|
||||
self.data.extend(chunk)
|
||||
encoded = object.to_list()
|
||||
encoded.insert(0, len(encoded))
|
||||
self.data.extend(encoded)
|
||||
|
||||
|
||||
class CurveArray(ObjectArray):
|
||||
|
||||
@classmethod
|
||||
def from_curve(cls, curve: Base) -> 'CurveArray':
|
||||
def from_curve(cls, curve: Base) -> "CurveArray":
|
||||
crv_array = cls()
|
||||
crv_array.data = curve.to_list()
|
||||
return crv_array
|
||||
|
||||
@classmethod
|
||||
def from_curves(cls, curves: List[Base]) -> 'CurveArray':
|
||||
def from_curves(cls, curves: List[Base]) -> "CurveArray":
|
||||
data = []
|
||||
for curve in curves:
|
||||
curve_list = curve.to_list()
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
from typing import List
|
||||
from specklepy.objects.geometry import Point, Vector
|
||||
from .base import Base
|
||||
|
||||
OTHER = "Objects.Other."
|
||||
|
||||
IDENTITY_TRANSFORM = [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
]
|
||||
|
||||
|
||||
class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
|
||||
name: str = None
|
||||
@@ -10,3 +31,169 @@ class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
|
||||
roughness: float = 1
|
||||
diffuse: int = -2894893 # light gray arbg
|
||||
emissive: int = -16777216 # black arbg
|
||||
|
||||
|
||||
class Transform(
|
||||
Base,
|
||||
speckle_type=OTHER + "Transform",
|
||||
serialize_ignore={"translation", "scaling", "is_identity"},
|
||||
):
|
||||
"""The 4x4 transformation matrix
|
||||
|
||||
The 3x3 sub-matrix determines scaling.
|
||||
The 4th column defines translation, where the last value is a divisor (usually equal to 1).
|
||||
"""
|
||||
|
||||
_value: List[float] = None
|
||||
|
||||
@property
|
||||
def value(self) -> List[float]:
|
||||
"""The transform matrix represented as a flat list of 16 floats"""
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: List[float]) -> None:
|
||||
try:
|
||||
value = [float(x) for x in value]
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(
|
||||
f"Could not create a Transform object with the requested value. Input must be a 16 element list of numbers. Value provided: {value}"
|
||||
)
|
||||
if len(value) != 16:
|
||||
raise ValueError(
|
||||
f"Could not create a Transform object: input list should be 16 floats long, but was {len(value)} long"
|
||||
)
|
||||
|
||||
self._value = value
|
||||
|
||||
@property
|
||||
def translation(self) -> List[float]:
|
||||
"""The final column of the matrix which defines the translation"""
|
||||
return [self._value[i] for i in (3, 7, 11, 15)]
|
||||
|
||||
@property
|
||||
def scaling(self) -> List[float]:
|
||||
"""The 3x3 scaling sub-matrix"""
|
||||
return [self._value[i] for i in (0, 1, 2, 4, 5, 6, 8, 9, 10)]
|
||||
|
||||
@property
|
||||
def is_identity(self) -> bool:
|
||||
return self.value == IDENTITY_TRANSFORM
|
||||
|
||||
def apply_to_point(self, point: Point) -> Point:
|
||||
"""Transform a single speckle Point
|
||||
|
||||
Arguments:
|
||||
point {Point} -- the speckle Point to transform
|
||||
|
||||
Returns:
|
||||
Point -- a new transformed point
|
||||
"""
|
||||
coords = self.apply_to_point_value([point.x, point.y, point.z])
|
||||
return Point(x=coords[0], y=coords[1], z=coords[2], units=point.units)
|
||||
|
||||
def apply_to_point_value(self, point_value: List[float]) -> List[float]:
|
||||
"""Transform a list of three floats representing a point
|
||||
|
||||
Arguments:
|
||||
point_value {List[float]} -- a list of 3 floats
|
||||
|
||||
Returns:
|
||||
List[float] -- the list with the transform applied
|
||||
"""
|
||||
transformed = [
|
||||
point_value[0] * self._value[i]
|
||||
+ point_value[1] * self._value[i + 1]
|
||||
+ point_value[2] * self._value[i + 2]
|
||||
+ self._value[i + 3]
|
||||
for i in range(0, 15, 4)
|
||||
]
|
||||
|
||||
return [transformed[i] / transformed[3] for i in range(3)]
|
||||
|
||||
def apply_to_points(self, points: List[Point]) -> List[Point]:
|
||||
"""Transform a list of speckle Points
|
||||
|
||||
Arguments:
|
||||
points {List[Point]} -- the list of speckle Points to transform
|
||||
|
||||
Returns:
|
||||
List[Point] -- a new list of transformed points
|
||||
"""
|
||||
return [self.apply_to_point(point) for point in points]
|
||||
|
||||
def apply_to_points_values(self, points_value: List[float]) -> List[float]:
|
||||
"""Transform a list of speckle Points
|
||||
|
||||
Arguments:
|
||||
points {List[float]} -- a flat list of floats representing points to transform
|
||||
|
||||
Returns:
|
||||
List[float] -- a new transformed list
|
||||
"""
|
||||
if len(points_value) % 3 != 0:
|
||||
raise ValueError(
|
||||
"Cannot apply transform as the points list is malformed: expected length to be multiple of 3"
|
||||
)
|
||||
transformed = []
|
||||
for i in range(0, len(points_value), 3):
|
||||
transformed.extend(self.apply_to_point_value(points_value[i : i + 3]))
|
||||
|
||||
return transformed
|
||||
|
||||
def apply_to_vector(self, vector: Vector) -> Vector:
|
||||
"""Transform a single speckle Vector
|
||||
|
||||
Arguments:
|
||||
point {Vector} -- the speckle Vector to transform
|
||||
|
||||
Returns:
|
||||
Vector -- a new transformed point
|
||||
"""
|
||||
coords = self.apply_to_vector_value([vector.x, vector.y, vector.z])
|
||||
return Vector(x=coords[0], y=coords[1], z=coords[2], units=vector.units)
|
||||
|
||||
def apply_to_vector_value(self, vector_value: List[float]) -> List[float]:
|
||||
"""Transform a list of three floats representing a vector
|
||||
|
||||
Arguments:
|
||||
vector_value {List[float]} -- a list of 3 floats
|
||||
|
||||
Returns:
|
||||
List[float] -- the list with the transform applied
|
||||
"""
|
||||
return [
|
||||
vector_value[0] * self._value[i]
|
||||
+ vector_value[1] * self._value[i + 1]
|
||||
+ vector_value[2] * self._value[i + 2]
|
||||
for i in range(0, 15, 4)
|
||||
][:3]
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, value: List[float] = None) -> "Transform":
|
||||
"""Returns a Transform object from a list of 16 numbers. If no value is provided, an identity transform will be returned.
|
||||
|
||||
Arguments:
|
||||
value {List[float]} -- the matrix as a flat list of 16 numbers (defaults to the identity transform)
|
||||
|
||||
Returns:
|
||||
Transform -- a complete transform object
|
||||
"""
|
||||
if not value:
|
||||
value = IDENTITY_TRANSFORM
|
||||
return cls(value=value)
|
||||
|
||||
|
||||
class BlockDefinition(
|
||||
Base, speckle_type=OTHER + "BlockDefinition", detachable={"geometry"}
|
||||
):
|
||||
name: str = None
|
||||
basePoint: Point = None
|
||||
geometry: List[Base] = None
|
||||
|
||||
|
||||
class BlockInstance(
|
||||
Base, speckle_type=OTHER + "BlockInstance", detachable={"blockDefinition"}
|
||||
):
|
||||
blockDefinition: BlockDefinition = None
|
||||
transform: Transform = 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"]
|
||||
|
||||
@@ -15,6 +16,7 @@ UNITS_STRINGS = {
|
||||
}
|
||||
|
||||
UNITS_ENCODINGS = {
|
||||
"none": 0,
|
||||
"mm": 1,
|
||||
"cm": 2,
|
||||
"m": 3,
|
||||
@@ -27,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:
|
||||
|
||||
@@ -60,14 +60,19 @@ class BaseObjectSerializer:
|
||||
chunkable = False
|
||||
detach = False
|
||||
|
||||
# skip nulls or props marked to be ignored with "__" or "_"
|
||||
if value is None or prop.startswith(("__", "_")):
|
||||
# skip props marked to be ignored with "__" or "_"
|
||||
if prop.startswith(("__", "_")):
|
||||
continue
|
||||
|
||||
# don't prepopulate id as this will mess up hashing
|
||||
if prop == "id":
|
||||
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
|
||||
if self.write_transports:
|
||||
dynamic_chunk_match = prop.startswith("@") and re.match(
|
||||
|
||||
@@ -13,21 +13,68 @@ from .batch_sender import BatchSender
|
||||
|
||||
|
||||
class ServerTransport(AbstractTransport):
|
||||
"""
|
||||
The `ServerTransport` is the vehicle through which you transport objects to and from a Speckle Server. Provide it to
|
||||
`operations.send()` or `operations.receive()`.
|
||||
|
||||
The `ServerTransport` can be authenticted two different ways:
|
||||
1. by providing a `SpeckleClient`
|
||||
2. by providing a `token` and `url`
|
||||
|
||||
```py
|
||||
from specklepy.api import operations
|
||||
from specklepy.transports.server import ServerTransport
|
||||
|
||||
# here's the data you want to send
|
||||
block = Block(length=2, height=4)
|
||||
|
||||
# next create the server transport - this is the vehicle through which you will send and receive
|
||||
transport = ServerTransport(stream_id=new_stream_id, client=client)
|
||||
|
||||
# this serialises the block and sends it to the transport
|
||||
hash = operations.send(base=block, transports=[transport])
|
||||
|
||||
# you can now create a commit on your stream with this object
|
||||
commid_id = client.commit.create(
|
||||
stream_id=new_stream_id,
|
||||
obj_id=hash,
|
||||
message="this is a block I made in speckle-py",
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
_name = "RemoteTransport"
|
||||
url: str = None
|
||||
stream_id: str = None
|
||||
saved_obj_count: int = 0
|
||||
session: requests.Session = None
|
||||
|
||||
def __init__(self, client: SpeckleClient, stream_id: str, **data: Any) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
stream_id: str,
|
||||
client: SpeckleClient = None,
|
||||
token: str = None,
|
||||
url: str = None,
|
||||
**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
|
||||
if client is None and token is None and url is None:
|
||||
raise SpeckleException(
|
||||
"You must provide either a client or a token and url to construct a ServerTransport."
|
||||
)
|
||||
|
||||
if client:
|
||||
if not client.me:
|
||||
raise SpeckleException(
|
||||
"The provided SpeckleClient was not authenticated."
|
||||
)
|
||||
token = client.me["token"]
|
||||
url = client.url
|
||||
|
||||
self.stream_id = stream_id
|
||||
self.url = url
|
||||
|
||||
token = client.me["token"]
|
||||
self._batch_sender = BatchSender(
|
||||
self.url, self.stream_id, token, max_batch_size_mb=1
|
||||
)
|
||||
|
||||
+15
-5
@@ -1,5 +1,5 @@
|
||||
from contextlib import ExitStack as does_not_raise
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import pytest
|
||||
from specklepy.api import operations
|
||||
@@ -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"
|
||||
@@ -87,9 +99,9 @@ class FrozenYoghurt(Base):
|
||||
"""Testing type checking"""
|
||||
|
||||
servings: int
|
||||
flavours: List[str] = None # list item types won't be checked
|
||||
flavours: List[str] # list item types won't be checked
|
||||
customer: str
|
||||
add_ons: Dict[str, float] # dict item types won't be checked
|
||||
add_ons: Optional[Dict[str, float]] # dict item types won't be checked
|
||||
price: float = 0.0
|
||||
|
||||
|
||||
@@ -111,5 +123,3 @@ def test_type_checking() -> None:
|
||||
order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
|
||||
|
||||
assert order.price == 7.0
|
||||
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class TestBranch:
|
||||
assert isinstance(branches, list)
|
||||
assert len(branches) == 2
|
||||
assert isinstance(branches[0], Branch)
|
||||
assert branches[0].name == branch.name
|
||||
assert branches[1].name == branch.name
|
||||
|
||||
def test_branch_update(self, client, stream, branch, updated_branch):
|
||||
updated = client.branch.update(
|
||||
|
||||
@@ -68,3 +68,20 @@ class TestCommit:
|
||||
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit_id)
|
||||
|
||||
assert deleted is True
|
||||
|
||||
def test_commit_marked_as_received(self, client, stream, mesh) -> None:
|
||||
commit = Commit(message="this commit should be received")
|
||||
commit.id = client.commit.create(
|
||||
stream_id=stream.id,
|
||||
object_id=mesh.id,
|
||||
message=commit.message,
|
||||
)
|
||||
|
||||
commit_marked_received = client.commit.received(
|
||||
stream.id,
|
||||
commit.id,
|
||||
source_application="pytest",
|
||||
message="testing received",
|
||||
)
|
||||
|
||||
assert commit_marked_received == True
|
||||
|
||||
+113
-50
@@ -5,11 +5,28 @@ import pytest
|
||||
from specklepy.api import operations
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.encoding import CurveArray, ObjectArray
|
||||
from specklepy.objects.geometry import (Arc, Box, Brep, BrepEdge, BrepFace,
|
||||
BrepLoop, BrepTrim, BrepTrimTypeEnum,
|
||||
Circle, Curve, Ellipse, Interval, Line,
|
||||
Mesh, Plane, Point, Polycurve,
|
||||
Polyline, Surface, Vector)
|
||||
from specklepy.objects.geometry import (
|
||||
Arc,
|
||||
Box,
|
||||
Brep,
|
||||
BrepEdge,
|
||||
BrepFace,
|
||||
BrepLoop,
|
||||
BrepTrim,
|
||||
BrepTrimTypeEnum,
|
||||
Circle,
|
||||
Curve,
|
||||
Ellipse,
|
||||
Interval,
|
||||
Line,
|
||||
Mesh,
|
||||
Plane,
|
||||
Point,
|
||||
Polycurve,
|
||||
Polyline,
|
||||
Surface,
|
||||
Vector,
|
||||
)
|
||||
from specklepy.transports.memory import MemoryTransport
|
||||
|
||||
|
||||
@@ -71,7 +88,7 @@ def arc(plane, interval):
|
||||
angleRadians=33,
|
||||
plane=plane,
|
||||
domain=interval,
|
||||
units='m',
|
||||
units="m",
|
||||
# These attributes are not handled in C#
|
||||
# bbox=None,
|
||||
# area=None,
|
||||
@@ -88,7 +105,7 @@ def circle(plane, interval):
|
||||
radius=22,
|
||||
plane=plane,
|
||||
domain=interval,
|
||||
units='m',
|
||||
units="m",
|
||||
# These attributes are not handled in C#
|
||||
# bbox=None,
|
||||
# area=None,
|
||||
@@ -103,7 +120,7 @@ def ellipse(plane, interval):
|
||||
secondRadius=22,
|
||||
plane=plane,
|
||||
domain=interval,
|
||||
units='m',
|
||||
units="m",
|
||||
# These attributes are not handled in C#
|
||||
# trimDomain=None,
|
||||
# bbox=None,
|
||||
@@ -118,7 +135,7 @@ def polyline(interval):
|
||||
value=[22, 44, 54.3, 99, 232, 21],
|
||||
closed=True,
|
||||
domain=interval,
|
||||
units='m',
|
||||
units="m",
|
||||
# These attributes are not handled in C#
|
||||
# bbox=None,
|
||||
# area=None,
|
||||
@@ -137,7 +154,7 @@ def curve(interval):
|
||||
points=[23, 21, 44, 43, 56, 76, 1, 3, 2],
|
||||
weights=[23, 11, 23],
|
||||
knots=[22, 45, 76, 11],
|
||||
units='m',
|
||||
units="m",
|
||||
# These attributes are not handled in C#
|
||||
# displayValue=None,
|
||||
# bbox=None,
|
||||
@@ -152,7 +169,7 @@ def polycurve(interval, curve, polyline):
|
||||
segments=[curve, polyline],
|
||||
domain=interval,
|
||||
closed=True,
|
||||
units='m',
|
||||
units="m",
|
||||
# These attributes are not handled in C#
|
||||
# bbox=None,
|
||||
# area=None,
|
||||
@@ -187,7 +204,7 @@ def surface(interval):
|
||||
domainV=interval,
|
||||
knotsU=[1.1, 2.2, 3.3, 4.4],
|
||||
knotsV=[9, 8, 7, 6, 5, 4.4],
|
||||
units='m',
|
||||
units="m",
|
||||
# These attributes are not handled in C#
|
||||
# bbox=None,
|
||||
# area=None,
|
||||
@@ -218,11 +235,7 @@ def brep_edge(interval):
|
||||
|
||||
@pytest.fixture()
|
||||
def brep_loop():
|
||||
return BrepLoop(
|
||||
FaceIndex=5,
|
||||
TrimIndices=[3, 4, 5],
|
||||
Type='unknown'
|
||||
)
|
||||
return BrepLoop(FaceIndex=5, TrimIndices=[3, 4, 5], Type="unknown")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -235,7 +248,7 @@ def brep_trim():
|
||||
LoopIndex=4,
|
||||
CurveIndex=7,
|
||||
IsoStatus=6,
|
||||
TrimType='Mated',
|
||||
TrimType="Mated",
|
||||
IsReversed=False,
|
||||
# These attributes are not handled in C#
|
||||
# Domain=None,
|
||||
@@ -243,10 +256,21 @@ def brep_trim():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def brep(mesh, box, surface, curve, polyline, circle, point,
|
||||
brep_edge, brep_loop, brep_trim, brep_face):
|
||||
def brep(
|
||||
mesh,
|
||||
box,
|
||||
surface,
|
||||
curve,
|
||||
polyline,
|
||||
circle,
|
||||
point,
|
||||
brep_edge,
|
||||
brep_loop,
|
||||
brep_trim,
|
||||
brep_face,
|
||||
):
|
||||
return Brep(
|
||||
provenance='pytest',
|
||||
provenance="pytest",
|
||||
bbox=box,
|
||||
area=32,
|
||||
volume=54,
|
||||
@@ -265,33 +289,57 @@ def brep(mesh, box, surface, curve, polyline, circle, point,
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def geometry_objects_dict(point, vector, plane, line, arc,
|
||||
circle, ellipse, polyline, curve,
|
||||
polycurve, surface, brep_trim):
|
||||
def geometry_objects_dict(
|
||||
point,
|
||||
vector,
|
||||
plane,
|
||||
line,
|
||||
arc,
|
||||
circle,
|
||||
ellipse,
|
||||
polyline,
|
||||
curve,
|
||||
polycurve,
|
||||
surface,
|
||||
brep_trim,
|
||||
):
|
||||
return {
|
||||
'point': point,
|
||||
'vector': vector,
|
||||
'plane': plane,
|
||||
'line': line,
|
||||
'arc': arc,
|
||||
'circle': circle,
|
||||
'ellipse': ellipse,
|
||||
'polyline': polyline,
|
||||
'curve': curve,
|
||||
'polycurve': polycurve,
|
||||
'surface': surface,
|
||||
'brep_trim': brep_trim
|
||||
"point": point,
|
||||
"vector": vector,
|
||||
"plane": plane,
|
||||
"line": line,
|
||||
"arc": arc,
|
||||
"circle": circle,
|
||||
"ellipse": ellipse,
|
||||
"polyline": polyline,
|
||||
"curve": curve,
|
||||
"polycurve": polycurve,
|
||||
"surface": surface,
|
||||
"brep_trim": brep_trim,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('object_name', [
|
||||
'point', 'vector', 'plane', 'line', 'arc', 'circle',
|
||||
'ellipse', 'polyline', 'curve', 'polycurve', 'surface', 'brep_trim'
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"object_name",
|
||||
[
|
||||
"point",
|
||||
"vector",
|
||||
"plane",
|
||||
"line",
|
||||
"arc",
|
||||
"circle",
|
||||
"ellipse",
|
||||
"polyline",
|
||||
"curve",
|
||||
"polycurve",
|
||||
"surface",
|
||||
"brep_trim",
|
||||
],
|
||||
)
|
||||
def test_to_and_from_list(object_name: str, geometry_objects_dict):
|
||||
object = geometry_objects_dict[object_name]
|
||||
assert hasattr(object, 'to_list')
|
||||
assert hasattr(object, 'from_list')
|
||||
assert hasattr(object, "to_list")
|
||||
assert hasattr(object, "from_list")
|
||||
|
||||
chunks = object.to_list()
|
||||
assert isinstance(chunks, list)
|
||||
@@ -306,8 +354,7 @@ def test_brep_surfaces_value_serialization(surface):
|
||||
assert brep.Surfaces == None
|
||||
assert brep.SurfacesValue == None
|
||||
brep.Surfaces = [surface, surface]
|
||||
assert brep.SurfacesValue == ObjectArray.from_objects(
|
||||
[surface, surface]).data
|
||||
assert brep.SurfacesValue == ObjectArray.from_objects([surface, surface]).data
|
||||
|
||||
brep.SurfacesValue = ObjectArray.from_objects([surface]).data
|
||||
assert len(brep.Surfaces) == 1
|
||||
@@ -341,16 +388,32 @@ def test_brep_curve3d_values_serialization(curve, polyline, circle):
|
||||
def test_brep_vertices_values_serialization():
|
||||
brep = Brep()
|
||||
brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3]
|
||||
brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units='mm').get_id()
|
||||
brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units='mm').get_id()
|
||||
brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units='mm').get_id()
|
||||
brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units="mm").get_id()
|
||||
brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units="mm").get_id()
|
||||
brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units="mm").get_id()
|
||||
|
||||
|
||||
def test_trims_value_serialization():
|
||||
brep = Brep()
|
||||
brep.TrimsValue = [
|
||||
0, 0, 0, 0, 0, 0, 1, 1, 1,
|
||||
1, 0, 0, 0, 0, 1, 2, 1, 0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
0,
|
||||
]
|
||||
|
||||
brep.Trims[0].get_id() == BrepTrim(
|
||||
@@ -383,7 +446,7 @@ def test_serialized_brep_attributes(brep: Brep):
|
||||
serialized = operations.serialize(brep, [transport])
|
||||
serialized_dict = json.loads(serialized)
|
||||
|
||||
removed_keys = ['Surfaces', 'Curve3D', 'Curve2D', 'Vertices', 'Trims']
|
||||
removed_keys = ["Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"]
|
||||
|
||||
for k in removed_keys:
|
||||
assert k not in serialized_dict.keys()
|
||||
|
||||
+3
-11
@@ -21,11 +21,9 @@ class TestObject:
|
||||
|
||||
def test_object_create(self, client, stream, base):
|
||||
transport = SQLiteTransport()
|
||||
s = BaseObjectSerializer(
|
||||
write_transports=[transport], read_transport=transport)
|
||||
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]
|
||||
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"
|
||||
@@ -43,12 +41,6 @@ class TestObject:
|
||||
|
||||
def test_object_array_decoder(self):
|
||||
array = ObjectArray()
|
||||
array.data = [
|
||||
5, 1, 1, 1, 1, 1,
|
||||
4, 1, 1, 1, 1,
|
||||
3, 1, 1, 1,
|
||||
2, 1, 1,
|
||||
1, 1
|
||||
]
|
||||
array.data = [5, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 3, 1, 1, 1, 2, 1, 1, 1, 1]
|
||||
|
||||
assert array.decode(decoder=sum) == [5, 4, 3, 2, 1]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import json
|
||||
from attr import has
|
||||
import pytest
|
||||
from specklepy.api import operations
|
||||
from specklepy.transports.server import ServerTransport
|
||||
@@ -53,6 +52,11 @@ class TestSerialization:
|
||||
def test_send_and_receive(self, client, sample_stream, mesh):
|
||||
transport = ServerTransport(client=client, stream_id=sample_stream.id)
|
||||
hash = operations.send(mesh, transports=[transport])
|
||||
|
||||
# also try constructing server transport with token and url
|
||||
transport = ServerTransport(
|
||||
stream_id=sample_stream.id, token=client.me["token"], url=client.url
|
||||
)
|
||||
# use a fresh memory transport to force receiving from remote
|
||||
received = operations.receive(
|
||||
hash, remote_transport=transport, local_transport=MemoryTransport()
|
||||
@@ -84,4 +88,4 @@ class TestSerialization:
|
||||
untyped = '{"foo": "bar"}'
|
||||
deserialised = operations.deserialize(untyped)
|
||||
|
||||
assert deserialised == {"foo": "bar"}
|
||||
assert deserialised == {"foo": "bar"}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
from typing import List
|
||||
import pytest
|
||||
from specklepy.api import operations
|
||||
from specklepy.objects.geometry import Point, Vector
|
||||
from specklepy.objects.other import (
|
||||
Transform,
|
||||
BlockInstance,
|
||||
BlockDefinition,
|
||||
IDENTITY_TRANSFORM,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def point():
|
||||
return Point(x=1, y=10, z=2)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def points():
|
||||
return [Point(x=1 + i, y=10 + i, z=2 + i) for i in range(5)]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def point_value():
|
||||
return [1, 10, 2]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def points_values():
|
||||
coords = []
|
||||
for i in range(5):
|
||||
coords.extend([1 + i, 10 + i, 2 + 1])
|
||||
return coords
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def vector():
|
||||
return Vector(x=1, y=10, z=2)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def vector_value():
|
||||
return [1, 1, 2]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def transform():
|
||||
"""Translates to [1, 2, 0] and scales z by 0.5"""
|
||||
return Transform.from_list(
|
||||
[
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
2.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.5,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_point_transform(point: Point, transform: Transform):
|
||||
new_point = transform.apply_to_point(point)
|
||||
|
||||
assert new_point.x == point.x + 1
|
||||
assert new_point.y == point.y + 2
|
||||
assert new_point.z == point.z * 0.5
|
||||
|
||||
|
||||
def test_points_transform(points: List[Point], transform: Transform):
|
||||
new_points = transform.apply_to_points(points)
|
||||
|
||||
for (i, new_point) in enumerate(new_points):
|
||||
assert new_point.x == points[i].x + 1
|
||||
assert new_point.y == points[i].y + 2
|
||||
assert new_point.z == points[i].z * 0.5
|
||||
|
||||
|
||||
def test_point_value_transform(point_value: List[float], transform: Transform):
|
||||
new_coords = transform.apply_to_point_value(point_value)
|
||||
|
||||
assert new_coords[0] == point_value[0] + 1
|
||||
assert new_coords[1] == point_value[1] + 2
|
||||
assert new_coords[2] == point_value[2] * 0.5
|
||||
|
||||
|
||||
def test_points_values_transform(points_values: List[float], transform: Transform):
|
||||
new_coords = transform.apply_to_points_values(points_values)
|
||||
|
||||
for i in range(0, len(points_values), 3):
|
||||
assert new_coords[i] == points_values[i] + 1
|
||||
assert new_coords[i + 1] == points_values[i + 1] + 2
|
||||
assert new_coords[i + 2] == points_values[i + 2] * 0.5
|
||||
|
||||
|
||||
def test_vector_transform(vector: Vector, transform: Transform):
|
||||
new_vector = transform.apply_to_vector(vector)
|
||||
|
||||
assert new_vector.x == vector.x
|
||||
assert new_vector.y == vector.y
|
||||
assert new_vector.z == vector.z * 0.5
|
||||
|
||||
|
||||
def test_vector_value_transform(vector_value: List[float], transform: Transform):
|
||||
new_coords = transform.apply_to_vector_value(vector_value)
|
||||
|
||||
assert new_coords[0] == vector_value[0]
|
||||
assert new_coords[1] == vector_value[1]
|
||||
assert new_coords[2] == vector_value[2] * 0.5
|
||||
|
||||
|
||||
def test_transform_fails_with_malformed_value():
|
||||
with pytest.raises(ValueError):
|
||||
Transform.from_list("asdf")
|
||||
with pytest.raises(ValueError):
|
||||
Transform.from_list([7, 8, 9])
|
||||
|
||||
|
||||
def test_transform_serialisation(transform: Transform):
|
||||
serialized = operations.serialize(transform)
|
||||
deserialized = operations.deserialize(serialized)
|
||||
|
||||
assert transform.get_id() == deserialized.get_id()
|
||||
@@ -18,6 +18,14 @@ class TestWrapper:
|
||||
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
|
||||
assert wrap.type == "branch"
|
||||
|
||||
def test_parse_nested_branch(self):
|
||||
wrap = StreamWrapper(
|
||||
"https://testing.speckle.dev/streams/4c3ce1459c/branches/izzy/dev"
|
||||
)
|
||||
|
||||
assert wrap.branch_name == "izzy/dev"
|
||||
assert wrap.type == "branch"
|
||||
|
||||
def test_parse_commit(self):
|
||||
wrap = StreamWrapper(
|
||||
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
|
||||
@@ -39,3 +47,33 @@ class TestWrapper:
|
||||
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893"
|
||||
)
|
||||
assert wrap.type == "commit"
|
||||
|
||||
#! NOTE: the following three tests may not pass locally if you have a `speckle.xyz` account in manager
|
||||
def test_get_client_without_auth(self):
|
||||
wrap = StreamWrapper(
|
||||
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
|
||||
)
|
||||
client = wrap.get_client()
|
||||
|
||||
assert client is not None
|
||||
|
||||
def test_get_new_client_with_token(self):
|
||||
wrap = StreamWrapper(
|
||||
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
|
||||
)
|
||||
client = wrap.get_client()
|
||||
client = wrap.get_client(token="super-secret-token")
|
||||
|
||||
assert client.me["token"] == "super-secret-token"
|
||||
|
||||
def test_get_transport_with_token(self):
|
||||
wrap = StreamWrapper(
|
||||
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
|
||||
)
|
||||
client = wrap.get_client()
|
||||
assert not client.me # unauthenticated bc no local accounts
|
||||
|
||||
transport = wrap.get_transport(token="super-secret-token")
|
||||
|
||||
assert transport is not None
|
||||
assert client.me["token"] == "super-secret-token"
|
||||
|
||||
Reference in New Issue
Block a user