Compare commits

..

46 Commits

Author SHA1 Message Date
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
izzy lyseggen 17aac0b552 Merge pull request #148 from specklesystems/izzy/allow-nulls
feat(serialisation): allow null values
2021-12-16 16:59:51 +00:00
izzy lyseggen c281a329a4 fix(serialisation): nulls & things ^^ 2021-12-16 16:57:36 +00:00
izzy lyseggen ca472716db feat(serialisation): allow null values 2021-12-16 16:41:09 +00:00
izzy lyseggen af50afe3ff Merge pull request #144 from specklesystems/izzy/transforms-rework
feat(objects): transforms and blocks!
2021-12-13 12:03:50 +00:00
izzy lyseggen b6493df77f test(transform): serialisation and vector transform tests 2021-12-13 11:48:05 +00:00
izzy lyseggen 59d3c8c3ea feat(objects): transform vectors & ignore fields 2021-12-13 11:47:49 +00:00
izzy lyseggen 4e3405f1fb fix(base): props with no setter 2021-12-13 11:47:10 +00:00
izzy lyseggen 49eabdd712 test(objects): transform create w malformed input 2021-12-10 18:34:40 +00:00
izzy lyseggen 96a31f0678 test(objects): transform methods 2021-12-10 18:29:35 +00:00
izzy lyseggen 91506b0b20 feat(objects): transforms and blocks! 2021-12-10 17:52:25 +00:00
izzy lyseggen b0de9e31b5 Merge pull request #143 from specklesystems/izzy/commits-hotfix
fix(client): add `branchName` to commits query
2021-12-10 11:50:24 +00:00
izzy lyseggen 2075783134 feat(models): add branchName to commit repr 2021-12-10 11:44:07 +00:00
izzy lyseggen 071f2449c3 fix(client): oopsie missing field in commit.list
thanks @mortenengen for the spot!
closes #142
2021-12-10 11:40:03 +00:00
izzy lyseggen a419664461 Merge pull request #140 from specklesystems/izzy/bump-deps
chore: update deps
2021-11-30 11:52:09 +00:00
izzy lyseggen 4a0c07009b chore: update deps 2021-11-30 11:50:34 +00:00
Gergő Jedlicska 682bcbfa9f Merge pull request #138 from specklesystems/gergo/ci_test_report
add codecov badge
2021-11-24 18:56:11 +01:00
Gergő Jedlicska ccf284e8fa add codecov badge 2021-11-24 18:53:44 +01:00
Gergő Jedlicska 23102a28ff Merge pull request #137 from specklesystems/gergo/ci_test_report
add circle_ci reporting
2021-11-24 18:46:25 +01:00
Gergő Jedlicska 5475edb253 coverage report to xml 2021-11-24 18:28:36 +01:00
Gergő Jedlicska c52f80c1ef add codecov upload 2021-11-24 18:21:39 +01:00
Gergő Jedlicska 21eecfa24c add circle_ci reporting 2021-11-24 16:47:48 +01:00
izzy lyseggen 5dde1bfcf1 Merge pull request #136 from specklesystems/izzy/stream-wrapper-fix
fix(wrapper): fix for nested branches
2021-11-17 16:12:42 +00:00
izzy lyseggen 82c9d874c9 test(branches): server now returns main first 2021-11-17 16:11:22 +00:00
izzy lyseggen 9acf2c8a92 fix(wrapper): fix for nested branches 2021-11-17 15:59:51 +00:00
Gergő Jedlicska 95012e60c1 Merge pull request #132 from specklesystems/commit-recieved
Commit recieved
2021-10-29 11:01:21 +02:00
Gergő Jedlicska 19b6500bbd Merge branch 'main' of github.com:specklesystems/specklepy into commit-recieved 2021-10-29 10:53:28 +02:00
Gergő Jedlicska 47a06e4630 version bump 2021-10-29 10:49:36 +02:00
Alan Rynne e5a8b40bb2 Merge pull request #131 from specklesystems/commit-recieved
commit recieved service implementation
2021-10-29 10:48:55 +02:00
Gergő Jedlicska 219456f5f8 circleci py310 is not working 2021-10-27 20:28:01 +02:00
Gergő Jedlicska d1544ae89f Merge branch 'commit-recieved' of github.com:specklesystems/specklepy into commit-recieved 2021-10-27 19:20:15 +02:00
Gergő Jedlicska 8f7d4b2ca7 add 3.10 as target 2021-10-27 19:20:04 +02:00
Gergő Jedlicska a7d31d4983 Delete stream_copy.py 2021-10-27 19:18:45 +02:00
Gergő Jedlicska a89b12a02c remove receive server test 2021-10-27 19:16:08 +02:00
Gergő Jedlicska 15ae68f5d7 commit received implementation 2021-10-27 14:13:49 +02:00
izzy lyseggen 0709cd99b5 Merge pull request #129 from specklesystems/izzy/token-auth
feat(wrapper/transport): token auth
2021-10-22 11:21:23 +02:00
izzy lyseggen faf06f7141 fix(server): set transport url 2021-10-21 13:12:19 +01:00
izzy lyseggen b54e09f811 test: server transport & wrapper token auth 2021-10-21 12:20:09 +01:00
izzy lyseggen 55b7e0d732 feat(wrapper): token auth for client and transport 2021-10-21 12:19:49 +01:00
izzy lyseggen 45c922679b feat(server): allow construction with token & url 2021-10-21 12:19:19 +01:00
izzy lyseggen b1c149382a Merge pull request #128 from specklesystems/izzy/cereal-fixes
fix(deserialisation): type check bug and brep encoding hotfix
2021-10-14 17:09:27 +02:00
izzy lyseggen 393e98c8c2 fix(encoding): add none unit type 2021-10-14 16:07:34 +01:00
izzy lyseggen 8376329cbb fix(base): type check error with optional generics
reported by rob on the forum:
https://speckle.community/t/issue-with-type-checking-in-pyhton/1861
2021-10-14 15:41:30 +01:00
izzy lyseggen 1567fe9e68 fix(breps): temp hotfix for curve encoding fail
addresses 🥒 Bug with brep receiving (curve encoding) #127
2021-10-14 15:38:51 +01:00
24 changed files with 1309 additions and 431 deletions
+13 -4
View File
@@ -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:
+4
View File
@@ -1,3 +1,7 @@
.tool-versions
.envrc
reports/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+3 -1
View File
@@ -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&amp;style=flat-square&amp;logo=discourse&amp;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&amp;logo=read-the-docs&amp;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&amp;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&amp;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
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -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]
+55 -11
View File
@@ -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)
+1 -1
View File
@@ -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__()
+39 -1
View File
@@ -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
+1 -1
View File
@@ -32,4 +32,4 @@ class GraphQLException(SpeckleException):
class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None:
super().__init__(*args)
super().__init__(*args)
+1 -1
View File
@@ -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
View File
@@ -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:
+35 -26
View File
@@ -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()
+187
View File
@@ -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
+9 -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"]
@@ -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(
+53 -6
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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(
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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]
+6 -2
View File
@@ -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"}
+132
View File
@@ -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()
+38
View File
@@ -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"