Compare commits

..

41 Commits

Author SHA1 Message Date
izzy lyseggen 9f5631cd90 feat(objects): mesh transform helper 2021-12-13 12:37:53 +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 1304 additions and 427 deletions
+13 -4
View File
@@ -2,14 +2,15 @@ version: 2.1
orbs: orbs:
python: circleci/python@1.3.2 python: circleci/python@1.3.2
codecov: codecov/codecov@3.2.2
jobs: jobs:
test: test:
docker: docker:
- image: "cimg/python:<<parameters.tag>>" - image: "cimg/python:<<parameters.tag>>"
- image: "circleci/node:12" - image: 'cimg/node:14.18'
- image: "circleci/redis:6" - image: 'circleci/redis:6'
- image: "circleci/postgres:12" - image: 'cimg/postgres:12.8'
environment: environment:
POSTGRES_DB: speckle2_test POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle POSTGRES_PASSWORD: speckle
@@ -38,7 +39,15 @@ jobs:
name: upgrade pip name: upgrade pip
- python/install-packages: - python/install-packages:
pkg-manager: poetry 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: deploy:
docker: docker:
+4
View File
@@ -1,3 +1,7 @@
.tool-versions
.envrc
reports/
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.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"><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://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 # 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] [tool.poetry]
name = "specklepy" name = "specklepy"
version = "2.1.0" version = "2.4.0"
description = "The Python SDK for Speckle 2.0" description = "The Python SDK for Speckle 2.0"
readme = "README.md" readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"] authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -12,16 +12,17 @@ homepage = "https://speckle.systems/"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.6.5" python = "^3.6.5"
pydantic = "^1.7.3" pydantic = "^1.8.2"
appdirs = "^1.4.4" appdirs = "^1.4.4"
gql = {version = ">=3.0.0a6", extras = ["all"], allow-prereleases = true} gql = {version = ">=3.0.0b1", extras = ["all"], allow-prereleases = true}
ujson = "^4.1.0" ujson = "^4.3.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = "^20.8b1" black = "^20.8b1"
isort = "^5.7.0" isort = "^5.7.0"
pytest = "^6.2.2" pytest = "^6.2.2"
pytest-ordering = "^0.6" pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
[tool.black] [tool.black]
+55 -11
View File
@@ -89,6 +89,28 @@ def get_default_account(base_path: str = None) -> Account:
class StreamWrapper: 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 stream_url: str = None
use_ssl: bool = True use_ssl: bool = True
host: str = None host: str = None
@@ -122,9 +144,9 @@ class StreamWrapper:
parsed = urlparse(url) parsed = urlparse(url)
self.host = parsed.netloc self.host = parsed.netloc
self.use_ssl = parsed.scheme == "https" 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( raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided." f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
) )
@@ -154,6 +176,9 @@ class StreamWrapper:
) )
def get_account(self) -> Account: 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: if self.account:
return self.account return self.account
@@ -164,23 +189,42 @@ class StreamWrapper:
return self.account return self.account
def get_client(self) -> SpeckleClient: def get_client(self, token: str = None) -> SpeckleClient:
if self.client: """
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 return self.client
if not self.account: if not self.account:
self.get_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) warn(f"No local account found for server {self.host}", SpeckleWarning)
return self.client return self.client
self.client.authenticate(self.account.token) self.client.authenticate(self.account.token if self.account else token)
return self.client return self.client
def get_transport(self) -> ServerTransport: def get_transport(self, token: str = None) -> ServerTransport:
if not self.client: """
self.get_client() Gets a server transport for this stream using an authenticated client. If there is no local account for this
return ServerTransport(self.client, self.stream_id) 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]] parents: Optional[List[str]]
def __repr__(self) -> 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: def __str__(self) -> str:
return self.__repr__() return self.__repr__()
+39 -1
View File
@@ -34,8 +34,8 @@ class Resource(ResourceBase):
stream(id: $stream_id) { stream(id: $stream_id) {
commit(id: $commit_id) { commit(id: $commit_id) {
id id
referencedObject
message message
referencedObject
authorId authorId
authorName authorName
authorAvatar authorAvatar
@@ -79,6 +79,7 @@ class Resource(ResourceBase):
authorId authorId
authorName authorName
authorAvatar authorAvatar
branchName
createdAt createdAt
sourceApplication sourceApplication
totalChildrenCount totalChildrenCount
@@ -185,3 +186,40 @@ class Resource(ResourceBase):
return self.make_request( return self.make_request(
query=query, params=params, return_type="commitDelete", parse_response=False 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
+34 -21
View File
@@ -1,6 +1,15 @@
import typing import typing
from typing import (Any, Callable, ClassVar, Dict, List, Optional, Set, Type, from typing import (
get_type_hints) Any,
Callable,
ClassVar,
Dict,
List,
Optional,
Set,
Type,
get_type_hints,
)
from warnings import warn from warnings import warn
from specklepy.logging.exceptions import SpeckleException from specklepy.logging.exceptions import SpeckleException
@@ -118,14 +127,12 @@ class _RegisteringBase:
except Exception: except Exception:
cls._attr_types = getattr(cls, "__annotations__", {}) cls._attr_types = getattr(cls, "__annotations__", {})
if chunkable: if chunkable:
chunkable = {k: v for k, v in chunkable.items() chunkable = {k: v for k, v in chunkable.items() if isinstance(v, int)}
if isinstance(v, int)}
cls._chunkable = dict(cls._chunkable, **chunkable) cls._chunkable = dict(cls._chunkable, **chunkable)
if detachable: if detachable:
cls._detachable = cls._detachable.union(detachable) cls._detachable = cls._detachable.union(detachable)
if serialize_ignore: if serialize_ignore:
cls._serialize_ignore = cls._serialize_ignore.union( cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
serialize_ignore)
super().__init_subclass__(**kwargs) super().__init_subclass__(**kwargs)
@@ -200,7 +207,7 @@ class Base(_RegisteringBase):
try: try:
attr.__set__(self, value) attr.__set__(self, value)
except AttributeError: except AttributeError:
pass # the prop probably doesn't have a setter return # the prop probably doesn't have a setter
super().__setattr__(name, value) super().__setattr__(name, value)
@classmethod @classmethod
@@ -215,15 +222,13 @@ class Base(_RegisteringBase):
try: try:
cls._attr_types = get_type_hints(cls) cls._attr_types = get_type_hints(cls)
except Exception as e: except Exception as e:
warn( warn(f"Could not update forward refs for class {cls.__name__}: {e}")
f"Could not update forward refs for class {cls.__name__}: {e}")
@classmethod @classmethod
def validate_prop_name(cls, name: str) -> None: def validate_prop_name(cls, name: str) -> None:
"""Validator for dynamic attribute names.""" """Validator for dynamic attribute names."""
if name in {"", "@"}: if name in {"", "@"}:
raise ValueError( raise ValueError("Invalid Name: Base member names cannot be empty strings")
"Invalid Name: Base member names cannot be empty strings")
if name.startswith("@@"): if name.startswith("@@"):
raise ValueError( raise ValueError(
"Invalid Name: Base member names cannot start with more than one '@'", "Invalid Name: Base member names cannot start with more than one '@'",
@@ -249,7 +254,12 @@ class Base(_RegisteringBase):
if t.__module__ == "typing": if t.__module__ == "typing":
origin = getattr(t, "__origin__") 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)): if not isinstance(t, (type, tuple)):
warn( warn(
f"Unrecognised type '{t}' provided for attribute '{name}'. Type will not been validated." f"Unrecognised type '{t}' provided for attribute '{name}'. Type will not been validated."
@@ -260,13 +270,16 @@ class Base(_RegisteringBase):
# to be friendly, we'll parse ints and strs into floats, but not the other way around # to be friendly, we'll parse ints and strs into floats, but not the other way around
# (to avoid unexpected rounding) # (to avoid unexpected rounding)
if t is float and isinstance(value, (int, str, float)): if isinstance(t, tuple):
try: t = t[0]
try:
if t is float:
return float(value) return float(value)
except ValueError: if t is str and value:
pass return str(value)
if t is str and value is not None: except ValueError:
return str(value) pass
raise SpeckleException( raise SpeckleException(
f"Cannot set '{self.__class__.__name__}.{name}': it expects type '{t.__name__}', but received type '{type(value).__name__}'" f"Cannot set '{self.__class__.__name__}.{name}': it expects type '{t.__name__}', but received type '{type(value).__name__}'"
@@ -327,7 +340,8 @@ class Base(_RegisteringBase):
def get_id(self, decompose: bool = False) -> str: 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 Note: the hash of a decomposed object differs from that of a non-decomposed object
@@ -337,8 +351,7 @@ class Base(_RegisteringBase):
Returns: Returns:
str -- the hash (id) of the fully serialized object str -- the hash (id) of the fully serialized object
""" """
from specklepy.serialization.base_object_serializer import \ from specklepy.serialization.base_object_serializer import BaseObjectSerializer
BaseObjectSerializer
serializer = BaseObjectSerializer() serializer = BaseObjectSerializer()
if decompose: if decompose:
+35 -26
View File
@@ -17,6 +17,7 @@ class CurveTypeEncoding(int, Enum):
@property @property
def object_class(self) -> Type: def object_class(self) -> Type:
from . import geometry from . import geometry
if self == self.Arc: if self == self.Arc:
return geometry.Arc return geometry.Arc
elif self == self.Circle: elif self == self.Circle:
@@ -32,7 +33,8 @@ class CurveTypeEncoding(int, Enum):
elif self == self.Polycurve: elif self == self.Polycurve:
return geometry.Polycurve return geometry.Polycurve
raise SpeckleException( 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]): def curve_from_list(args: List[float]):
@@ -41,61 +43,68 @@ def curve_from_list(args: List[float]):
class ObjectArray: class ObjectArray:
def __init__(self) -> None: def __init__(self) -> None:
self.data = [] self.data = []
@classmethod @classmethod
def from_objects(cls, objects: List[Base]) -> 'ObjectArray': def from_objects(cls, objects: List[Base]) -> "ObjectArray":
data_chunk = cls() data_list = cls()
if len(objects) == 0: if not objects:
return data_chunk return data_list
speckle_type = objects[0].speckle_type speckle_type = objects[0].speckle_type
for obj in objects: for obj in objects:
if speckle_type != obj.speckle_type: if speckle_type != obj.speckle_type:
raise SpeckleException( raise SpeckleException(
'All objects in chunk should have the same speckle_type. ' "All objects in chunk should have the same speckle_type. "
f'Found {speckle_type} and {obj.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 @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 index = 0
unchunked_data = []
while index < len(data): while index < len(data):
chunk_length = data[index] item_length = data[index]
chunk_start = int(index + 1) item_start = index + 1
chunk_end = int(chunk_start + chunk_length) item_end = item_start + item_length
chunk_data = data[chunk_start:chunk_end] item_data = data[item_start:item_end]
decoded_data = decoder(chunk_data) index = item_end
unchunked_data.append(decoded_data) # TODO: investigate what's going on w this fail
index = chunk_end try:
return unchunked_data decoded_data = decoder(item_data)
bases.append(decoded_data)
except ValueError:
continue
return bases
def decode(self, decoder: Callable[[List[Any]], Any]): def decode(self, decoder: Callable[[List[Any]], Any]):
return self.decode_data(data=self.data, decoder=decoder) return self.decode_data(data=self.data, decoder=decoder)
def encode_object(self, object: Base): def encode_object(self, object: Base):
chunk = object.to_list() encoded = object.to_list()
chunk.insert(0, len(chunk)) encoded.insert(0, len(encoded))
self.data.extend(chunk) self.data.extend(encoded)
class CurveArray(ObjectArray): class CurveArray(ObjectArray):
@classmethod @classmethod
def from_curve(cls, curve: Base) -> 'CurveArray': def from_curve(cls, curve: Base) -> "CurveArray":
crv_array = cls() crv_array = cls()
crv_array.data = curve.to_list() crv_array.data = curve.to_list()
return crv_array return crv_array
@classmethod @classmethod
def from_curves(cls, curves: List[Base]) -> 'CurveArray': def from_curves(cls, curves: List[Base]) -> "CurveArray":
data = [] data = []
for curve in curves: for curve in curves:
curve_list = curve.to_list() curve_list = curve.to_list()
+9
View File
@@ -391,6 +391,15 @@ class Mesh(
area: float = None area: float = None
volume: float = None volume: float = None
def transform_to(self, transform: "Transform") -> "Mesh":
mesh = Mesh(vertices=transform.apply_to_points_values(self.vertices))
for attr in set(self.get_serializable_attributes()) - {"vertices"}:
orig_val = getattr(self, attr, None)
if orig_val:
setattr(mesh, attr, orig_val)
return mesh
class Surface(Base, speckle_type=GEOMETRY + "Surface"): class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: int = None degreeU: int = None
+187
View File
@@ -1,7 +1,28 @@
from typing import List
from specklepy.objects.geometry import Point, Vector
from .base import Base from .base import Base
OTHER = "Objects.Other." 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"): class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
name: str = None name: str = None
@@ -10,3 +31,169 @@ class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
roughness: float = 1 roughness: float = 1
diffuse: int = -2894893 # light gray arbg diffuse: int = -2894893 # light gray arbg
emissive: int = -16777216 # black 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
View File
@@ -15,6 +15,7 @@ UNITS_STRINGS = {
} }
UNITS_ENCODINGS = { UNITS_ENCODINGS = {
"none": 0,
"mm": 1, "mm": 1,
"cm": 2, "cm": 2,
"m": 3, "m": 3,
+53 -6
View File
@@ -13,21 +13,68 @@ from .batch_sender import BatchSender
class ServerTransport(AbstractTransport): 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" _name = "RemoteTransport"
url: str = None url: str = None
stream_id: str = None stream_id: str = None
saved_obj_count: int = 0 saved_obj_count: int = 0
session: requests.Session = None 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) super().__init__(**data)
# TODO: replace client with account or some other auth avenue # TODO: replace client with account or some other auth avenue
if not client.me: if client is None and token is None and url is None:
raise SpeckleException("The provided SpeckleClient was not authenticated.") raise SpeckleException(
self.url = client.url "You must provide either a client or a token and url to construct a ServerTransport."
self.stream_id = stream_id )
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._batch_sender = BatchSender(
self.url, self.stream_id, token, max_batch_size_mb=1 self.url, self.stream_id, token, max_batch_size_mb=1
) )
+3 -5
View File
@@ -1,5 +1,5 @@
from contextlib import ExitStack as does_not_raise from contextlib import ExitStack as does_not_raise
from typing import Dict, List from typing import Dict, List, Optional
import pytest import pytest
from specklepy.api import operations from specklepy.api import operations
@@ -87,9 +87,9 @@ class FrozenYoghurt(Base):
"""Testing type checking""" """Testing type checking"""
servings: int 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 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 price: float = 0.0
@@ -111,5 +111,3 @@ def test_type_checking() -> None:
order.flavours = ["strawberry", "lychee", "peach", "pineapple"] order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
assert order.price == 7.0 assert order.price == 7.0
+1 -1
View File
@@ -58,7 +58,7 @@ class TestBranch:
assert isinstance(branches, list) assert isinstance(branches, list)
assert len(branches) == 2 assert len(branches) == 2
assert isinstance(branches[0], Branch) 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): def test_branch_update(self, client, stream, branch, updated_branch):
updated = client.branch.update( 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) deleted = client.commit.delete(stream_id=stream.id, commit_id=commit_id)
assert deleted is True 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.api import operations
from specklepy.objects.base import Base from specklepy.objects.base import Base
from specklepy.objects.encoding import CurveArray, ObjectArray from specklepy.objects.encoding import CurveArray, ObjectArray
from specklepy.objects.geometry import (Arc, Box, Brep, BrepEdge, BrepFace, from specklepy.objects.geometry import (
BrepLoop, BrepTrim, BrepTrimTypeEnum, Arc,
Circle, Curve, Ellipse, Interval, Line, Box,
Mesh, Plane, Point, Polycurve, Brep,
Polyline, Surface, Vector) BrepEdge,
BrepFace,
BrepLoop,
BrepTrim,
BrepTrimTypeEnum,
Circle,
Curve,
Ellipse,
Interval,
Line,
Mesh,
Plane,
Point,
Polycurve,
Polyline,
Surface,
Vector,
)
from specklepy.transports.memory import MemoryTransport from specklepy.transports.memory import MemoryTransport
@@ -71,7 +88,7 @@ def arc(plane, interval):
angleRadians=33, angleRadians=33,
plane=plane, plane=plane,
domain=interval, domain=interval,
units='m', units="m",
# These attributes are not handled in C# # These attributes are not handled in C#
# bbox=None, # bbox=None,
# area=None, # area=None,
@@ -88,7 +105,7 @@ def circle(plane, interval):
radius=22, radius=22,
plane=plane, plane=plane,
domain=interval, domain=interval,
units='m', units="m",
# These attributes are not handled in C# # These attributes are not handled in C#
# bbox=None, # bbox=None,
# area=None, # area=None,
@@ -103,7 +120,7 @@ def ellipse(plane, interval):
secondRadius=22, secondRadius=22,
plane=plane, plane=plane,
domain=interval, domain=interval,
units='m', units="m",
# These attributes are not handled in C# # These attributes are not handled in C#
# trimDomain=None, # trimDomain=None,
# bbox=None, # bbox=None,
@@ -118,7 +135,7 @@ def polyline(interval):
value=[22, 44, 54.3, 99, 232, 21], value=[22, 44, 54.3, 99, 232, 21],
closed=True, closed=True,
domain=interval, domain=interval,
units='m', units="m",
# These attributes are not handled in C# # These attributes are not handled in C#
# bbox=None, # bbox=None,
# area=None, # area=None,
@@ -137,7 +154,7 @@ def curve(interval):
points=[23, 21, 44, 43, 56, 76, 1, 3, 2], points=[23, 21, 44, 43, 56, 76, 1, 3, 2],
weights=[23, 11, 23], weights=[23, 11, 23],
knots=[22, 45, 76, 11], knots=[22, 45, 76, 11],
units='m', units="m",
# These attributes are not handled in C# # These attributes are not handled in C#
# displayValue=None, # displayValue=None,
# bbox=None, # bbox=None,
@@ -152,7 +169,7 @@ def polycurve(interval, curve, polyline):
segments=[curve, polyline], segments=[curve, polyline],
domain=interval, domain=interval,
closed=True, closed=True,
units='m', units="m",
# These attributes are not handled in C# # These attributes are not handled in C#
# bbox=None, # bbox=None,
# area=None, # area=None,
@@ -187,7 +204,7 @@ def surface(interval):
domainV=interval, domainV=interval,
knotsU=[1.1, 2.2, 3.3, 4.4], knotsU=[1.1, 2.2, 3.3, 4.4],
knotsV=[9, 8, 7, 6, 5, 4.4], knotsV=[9, 8, 7, 6, 5, 4.4],
units='m', units="m",
# These attributes are not handled in C# # These attributes are not handled in C#
# bbox=None, # bbox=None,
# area=None, # area=None,
@@ -218,11 +235,7 @@ def brep_edge(interval):
@pytest.fixture() @pytest.fixture()
def brep_loop(): def brep_loop():
return BrepLoop( return BrepLoop(FaceIndex=5, TrimIndices=[3, 4, 5], Type="unknown")
FaceIndex=5,
TrimIndices=[3, 4, 5],
Type='unknown'
)
@pytest.fixture() @pytest.fixture()
@@ -235,7 +248,7 @@ def brep_trim():
LoopIndex=4, LoopIndex=4,
CurveIndex=7, CurveIndex=7,
IsoStatus=6, IsoStatus=6,
TrimType='Mated', TrimType="Mated",
IsReversed=False, IsReversed=False,
# These attributes are not handled in C# # These attributes are not handled in C#
# Domain=None, # Domain=None,
@@ -243,10 +256,21 @@ def brep_trim():
@pytest.fixture @pytest.fixture
def brep(mesh, box, surface, curve, polyline, circle, point, def brep(
brep_edge, brep_loop, brep_trim, brep_face): mesh,
box,
surface,
curve,
polyline,
circle,
point,
brep_edge,
brep_loop,
brep_trim,
brep_face,
):
return Brep( return Brep(
provenance='pytest', provenance="pytest",
bbox=box, bbox=box,
area=32, area=32,
volume=54, volume=54,
@@ -265,33 +289,57 @@ def brep(mesh, box, surface, curve, polyline, circle, point,
@pytest.fixture @pytest.fixture
def geometry_objects_dict(point, vector, plane, line, arc, def geometry_objects_dict(
circle, ellipse, polyline, curve, point,
polycurve, surface, brep_trim): vector,
plane,
line,
arc,
circle,
ellipse,
polyline,
curve,
polycurve,
surface,
brep_trim,
):
return { return {
'point': point, "point": point,
'vector': vector, "vector": vector,
'plane': plane, "plane": plane,
'line': line, "line": line,
'arc': arc, "arc": arc,
'circle': circle, "circle": circle,
'ellipse': ellipse, "ellipse": ellipse,
'polyline': polyline, "polyline": polyline,
'curve': curve, "curve": curve,
'polycurve': polycurve, "polycurve": polycurve,
'surface': surface, "surface": surface,
'brep_trim': brep_trim "brep_trim": brep_trim,
} }
@pytest.mark.parametrize('object_name', [ @pytest.mark.parametrize(
'point', 'vector', 'plane', 'line', 'arc', 'circle', "object_name",
'ellipse', 'polyline', 'curve', 'polycurve', 'surface', 'brep_trim' [
]) "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): def test_to_and_from_list(object_name: str, geometry_objects_dict):
object = geometry_objects_dict[object_name] object = geometry_objects_dict[object_name]
assert hasattr(object, 'to_list') assert hasattr(object, "to_list")
assert hasattr(object, 'from_list') assert hasattr(object, "from_list")
chunks = object.to_list() chunks = object.to_list()
assert isinstance(chunks, list) assert isinstance(chunks, list)
@@ -306,8 +354,7 @@ def test_brep_surfaces_value_serialization(surface):
assert brep.Surfaces == None assert brep.Surfaces == None
assert brep.SurfacesValue == None assert brep.SurfacesValue == None
brep.Surfaces = [surface, surface] brep.Surfaces = [surface, surface]
assert brep.SurfacesValue == ObjectArray.from_objects( assert brep.SurfacesValue == ObjectArray.from_objects([surface, surface]).data
[surface, surface]).data
brep.SurfacesValue = ObjectArray.from_objects([surface]).data brep.SurfacesValue = ObjectArray.from_objects([surface]).data
assert len(brep.Surfaces) == 1 assert len(brep.Surfaces) == 1
@@ -341,16 +388,32 @@ def test_brep_curve3d_values_serialization(curve, polyline, circle):
def test_brep_vertices_values_serialization(): def test_brep_vertices_values_serialization():
brep = Brep() brep = Brep()
brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3] 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[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[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[2].get_id() == Point(x=3, y=3, z=3, _units="mm").get_id()
def test_trims_value_serialization(): def test_trims_value_serialization():
brep = Brep() brep = Brep()
brep.TrimsValue = [ brep.TrimsValue = [
0, 0, 0, 0, 0, 0, 1, 1, 1, 0,
1, 0, 0, 0, 0, 1, 2, 1, 0, 0,
0,
0,
0,
0,
1,
1,
1,
1,
0,
0,
0,
0,
1,
2,
1,
0,
] ]
brep.Trims[0].get_id() == BrepTrim( brep.Trims[0].get_id() == BrepTrim(
@@ -383,7 +446,7 @@ def test_serialized_brep_attributes(brep: Brep):
serialized = operations.serialize(brep, [transport]) serialized = operations.serialize(brep, [transport])
serialized_dict = json.loads(serialized) serialized_dict = json.loads(serialized)
removed_keys = ['Surfaces', 'Curve3D', 'Curve2D', 'Vertices', 'Trims'] removed_keys = ["Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"]
for k in removed_keys: for k in removed_keys:
assert k not in serialized_dict.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): def test_object_create(self, client, stream, base):
transport = SQLiteTransport() transport = SQLiteTransport()
s = BaseObjectSerializer( s = BaseObjectSerializer(write_transports=[transport], read_transport=transport)
write_transports=[transport], read_transport=transport)
_, base_dict = s.traverse_base(base) _, base_dict = s.traverse_base(base)
obj_id = client.object.create( obj_id = client.object.create(stream_id=stream.id, objects=[base_dict])[0]
stream_id=stream.id, objects=[base_dict])[0]
assert isinstance(obj_id, str) assert isinstance(obj_id, str)
assert base_dict["@detach"]["speckle_type"] == "reference" assert base_dict["@detach"]["speckle_type"] == "reference"
@@ -43,12 +41,6 @@ class TestObject:
def test_object_array_decoder(self): def test_object_array_decoder(self):
array = ObjectArray() array = ObjectArray()
array.data = [ array.data = [5, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 3, 1, 1, 1, 2, 1, 1, 1, 1]
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] assert array.decode(decoder=sum) == [5, 4, 3, 2, 1]
+5 -1
View File
@@ -1,5 +1,4 @@
import json import json
from attr import has
import pytest import pytest
from specklepy.api import operations from specklepy.api import operations
from specklepy.transports.server import ServerTransport from specklepy.transports.server import ServerTransport
@@ -53,6 +52,11 @@ class TestSerialization:
def test_send_and_receive(self, client, sample_stream, mesh): def test_send_and_receive(self, client, sample_stream, mesh):
transport = ServerTransport(client=client, stream_id=sample_stream.id) transport = ServerTransport(client=client, stream_id=sample_stream.id)
hash = operations.send(mesh, transports=[transport]) 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 # use a fresh memory transport to force receiving from remote
received = operations.receive( received = operations.receive(
hash, remote_transport=transport, local_transport=MemoryTransport() hash, remote_transport=transport, local_transport=MemoryTransport()
+151
View File
@@ -0,0 +1,151 @@
from typing import List
import pytest
from specklepy.api import operations
from specklepy.objects.geometry import Mesh, 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 mesh():
return Mesh(
vertices=[-7, 5, 1, -8, 4, 0, -7, 3, 0, -6, 4, 0],
faces=[1, 1, 2, 3, 0],
units="feet",
)
@pytest.fixture()
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()
def test_mesh_transform(mesh: Mesh, transform: Transform):
new_mesh = mesh.transform_to(transform)
assert mesh.vertices != new_mesh.vertices
new_mesh.vertices = mesh.vertices
assert mesh.get_id() == new_mesh.get_id()
+38
View File
@@ -18,6 +18,14 @@ class TestWrapper:
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?" assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
assert wrap.type == "branch" 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): def test_parse_commit(self):
wrap = StreamWrapper( wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792" "https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
@@ -39,3 +47,33 @@ class TestWrapper:
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893" "https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893"
) )
assert wrap.type == "commit" 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"