Compare commits

..

1 Commits

Author SHA1 Message Date
izzy lyseggen 3246edda8f feat(objects): add value property to points 2021-06-17 18:23:35 +01:00
31 changed files with 355 additions and 1960 deletions
+8 -11
View File
@@ -4,7 +4,7 @@ orbs:
python: circleci/python@1.3.2
jobs:
test:
build:
docker:
- image: "cimg/python:<<parameters.tag>>"
- image: "circleci/node:12"
@@ -14,7 +14,7 @@ jobs:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
- image: "speckle/speckle-server"
- image: "speckle/speckle-server:5f8cf11cba07ea6a54000243f9cb343b61cbba13"
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "localhost"
@@ -38,30 +38,27 @@ jobs:
name: upgrade pip
- python/install-packages:
pkg-manager: poetry
- run: poetry run pytest
- run: poetry run pytest --version
deploy:
docker:
- image: "circleci/python:3.8"
steps:
- checkout
- run: python patch_version.py $CIRCLE_TAG
- run: poetry build
- run: poetry publish -u specklesystems -p $PYPI_PASSWORD
workflows:
main:
jobs:
- test:
jobs:
- build:
matrix:
parameters:
tag: ["3.6", "3.7", "3.8", "3.9"]
filters:
tags:
only: /.*/
publish:
jobs:
- deploy:
requires:
- test
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
-78
View File
@@ -1,78 +0,0 @@
name: Update issue Status
on:
issues:
types: [closed]
jobs:
update_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo "$PROJECT_ID"
echo "$STATUS_FIELD_ID"
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
echo "$DONE_ID"
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Update Status
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
set_status: updateProjectNextItemField(
input: {
projectId: $project
itemId: $id
fieldId: $status
value: $value
}
) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
-50
View File
@@ -1,50 +0,0 @@
name: Move new issues into Project
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
- name: Add Issue to project
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
+111 -44
View File
@@ -1,53 +1,17 @@
<h1 align="center">
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
Speckle | specklepy 🐍
</h1>
<h3 align="center">
The Python SDK
</h3>
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
# speckle-py 🥧
<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>
[![Twitter Follow](https://img.shields.io/twitter/follow/SpeckleSystems?style=social)](https://twitter.com/SpeckleSystems) [![Community forum users](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.speckle.works&style=flat-square&logo=discourse&logoColor=white)](https://discourse.speckle.works) [![website](https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square)](https://speckle.systems) [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/)
# About Speckle
## Introduction
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
> ⚠ This is the start of the Python client for Speckle 2.0. It is currently quite nebulous and may be trashed and rebuilt at any moment! It is compatible with Python 3.6+ ⚠
>
### Features
## Documentation
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
- **Collaboration:** share your designs collaborate with others
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
- **Real time:** get real time updates and notifications and changes
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
Comprehensive developer and user documentation can be found in our:
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
# Repo structure
## Usage
Send and receive data from a Speckle Server with `operations`, interact with the Speckle API with the `SpeckleClient`, create and extend your own custom Speckle Objects with `Base`, and more!
Head to the [**📚 specklepy docs**](https://speckle.guide/dev/python.html) for more information and usage examples.
#### 📚 [Speckle Docs website](https://speckle.guide/dev/)
## Developing & Debugging
@@ -70,6 +34,109 @@ It may be helpful to know where the local accounts and object cache dbs are stor
- Linux: `$XDG_DATA_HOME` or by default `~/.local/share/Speckle`
- Mac: `~/.config/Speckle`
## Overview of functionality
The `SpeckleClient` is the entry point for interacting with the GraphQL API. You'll need to have a running server to use this.
```py
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account, get_local_accounts
all_accounts = get_local_accounts() # get back a list
account = get_default_account()
client = SpeckleClient(host="localhost:3000", use_ssl=False)
# client = SpeckleClient(host="yourserver.com") or whatever your host is
client.authenticate(account.token)
```
Interacting with streams is meant to be intuitive and evocative of PySpeckle 1.0
```py
# get your streams
stream_list = client.stream.list()
# search your streams
results = client.user.search("mech")
# create a stream
new_stream_id = client.stream.create(name="a shiny new stream")
# get a stream
new_stream = client.stream.get(id=new_stream_id)
```
New in 2.0: commits! Here are some basic commit interactions.
```py
# get list of commits
commits = client.commit.list("stream id")
# get a specific commit
commit = client.commit.get("stream id", "commit id")
# create a commit
commit_id = client.commit.create("stream id", "object id", "this is a commit message to describe the commit")
# delete a commit
deleted = client.commit.delete("stream id", "commit id")
```
The `BaseObjectSerializer` is used for decomposing and serializing `Base` objects so they can be sent / received to the server. You can use it directly to get the id (hash) and a serializable object representation of the decomposed `Base`. You can learn more about the Speckle `Base` object [here](https://discourse.speckle.works/t/core-2-0-the-base-object/782) and the decomposition API [here](https://discourse.speckle.works/t/core-2-0-decomposition-api/911).
```py
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
detached_base = Base()
detached_base.name = "this will get detached"
base_obj = Base()
base_obj.name = "my base"
base_obj["@nested"] = detached_base
serializer = BaseObjectSerializer()
hash, obj_dict = serializer.traverse_base(base_obj)
```
If you use the `operations`, you will not need to interact with the serializer directly as this will be taken care of for you. You will just need to provide a transport to indicate where the objects should be sent / received from. At the moment, just the `MemoryTransport` and the `ServerTransport` are fully functional at the moment. If you'd like to learn more about Transports in Speckle 2.0, have a look [here](https://discourse.speckle.works/t/core-2-0-transports/919).
```py
from specklepy.transports.memory import MemoryTransport
from specklepy.api import operations
transport = MemoryTransport()
# this serialises the object and sends it to the transport
hash = operations.send(base=base_obj, transports=[transport])
# if the object had detached objects, you can see these as well
saved_objects = transport.objects # a dict with the obj hash as the key
# this receives and object from the given transport, deserialises it, and recomposes it into a base object
received_base = operations.receive(obj_id=hash, remote_transport=transport)
```
You can also use the GraphQL API to send and receive objects.
```py
# create a test base object
test_base = Base()
test_base.testing = "a test base obj"
# run it through the serialiser
s = BaseObjectSerializer()
hash, obj = s.traverse_base(test_base)
# send it to the server
objCreate = client.object.create(stream_id="stream id", objects=[obj])
received_base = client.object.get("stream id", hash)
```
This doc is not complete - there's more to see so have a dive into the code and play around! Please feel free to provide feedback, submit issues, or discuss new features ✨
## Contributing
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) for an overview of the best practices we try to follow.
-31
View File
@@ -1,31 +0,0 @@
import re
import sys
def patch(tag):
print(f"Patching version: {tag}")
with open("pyproject.toml", "r") as f:
lines = f.readlines()
if "version" not in lines[2]:
raise Exception(f"Invalid pyproject.toml. Could not patch version.")
lines[2] = f'version = "{tag}"\n'
with open("pyproject.toml", "w") as file:
file.writelines(lines)
def main():
if len(sys.argv) < 2:
return
tag = sys.argv[1]
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
raise ValueError(f"Invalid tag provided: {tag}")
patch(tag)
if __name__ == "__main__":
main()
Generated
+1 -32
View File
@@ -371,14 +371,6 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "ujson"
version = "4.1.0"
description = "Ultra fast JSON encoder and decoder for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "urllib3"
version = "1.26.5"
@@ -428,7 +420,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake
[metadata]
lock-version = "1.1"
python-versions = "^3.6.5"
content-hash = "728db0014dfb8a83c50fe5ce6e86d068c4c87d319d50fb1e8135e63507713f30"
content-hash = "84e846c1bb02924ceada07406e95032e0632d229a36657ba2b85129e68f1526d"
[metadata.files]
aiohttp = [
@@ -719,29 +711,6 @@ typing-extensions = [
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
ujson = [
{file = "ujson-4.1.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:148680f2bc6e52f71c56908b65f59b36a13611ac2f75a86f2cb2bce2b2c2588c"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1c2fb32976982e4e75ca0843a1e7b2254b8c5d8c45d979ebf2db29305b4fa31"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:971d4b450e689bfec8ad6b22060fb9b9bec1e0860dbdf0fa7cfe4068adbc5f58"},
{file = "ujson-4.1.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f453480b275192ae40ef350a4e8288977f00b02e504ed34245ebd12d633620cb"},
{file = "ujson-4.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f135db442e5470d9065536745968efc42a60233311c8509b9327bcd59a8821c7"},
{file = "ujson-4.1.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:2251fc9395ba4498cbdc48136a179b8f20914fa8b815aa9453b20b48ad120f43"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9005d0d952d0c1b3dff5cdb79df2bde35a3499e2de3f708a22c45bbb4089a1f6"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:117855246a9ea3f61f3b69e5ca1b1d11d622b3126f50a0ec08b577cb5c87e56e"},
{file = "ujson-4.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:989bed422e7e20c7ba740a4e1bbeb28b3b6324e04f023ea238a2e5449fc53668"},
{file = "ujson-4.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:44993136fd2ecade747b6db95917e4f015a3279e09a08113f70cbbd0d241e66a"},
{file = "ujson-4.1.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9e962df227fd1d851ff095382a9f8432c2470c3ee640f02ae14231dc5728e6f3"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be6013cda610c5149fb80a84ee815b210aa2e7fe4edf1d2bce42c02336715208"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41b7e5422184249b5b94d1571206f76e5d91e8d721ce51abe341a88f41dd6692"},
{file = "ujson-4.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:807bb0585f30a650ec981669827721ed3ee1ee24f2c6f333a64982a40eb66b82"},
{file = "ujson-4.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d2955dd5cce0e76ba56786d647aaedca2cebb75eda9f0ec1787110c3646751a8"},
{file = "ujson-4.1.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a873c93d43f9bd14d9e9a6d2c6eb7aae4aad9717fe40c748d0cd4b6ed7767c62"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8fe9bbeca130debb10eea7910433a0714c8efc057fad36353feccb87c1d07f"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:81a49dbf176ae041fc86d2da564f5b9b46faf657306035632da56ecfd7203193"},
{file = "ujson-4.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1fb2455e62f20ab4a6d49f78b5dc4ff99c72fdab9466e761120e9757fa35f4d7"},
{file = "ujson-4.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:44db30b8fc52e70a6f67def11804f74818addafef0a65cd7f0abb98b7830920f"},
{file = "ujson-4.1.0.tar.gz", hash = "sha256:22b63ec4409f0d2f2c4c9d5aa331997e02470b7a15a3233f3cc32f2f9b92d58c"},
]
urllib3 = [
{file = "urllib3-1.26.5-py2.py3-none-any.whl", hash = "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c"},
{file = "urllib3-1.26.5.tar.gz", hash = "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"},
+1 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.1.0"
version = "2.2.3"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -15,7 +15,6 @@ python = "^3.6.5"
pydantic = "^1.7.3"
appdirs = "^1.4.4"
gql = {version = ">=3.0.0a6", extras = ["all"], allow-prereleases = true}
ujson = "^4.1.0"
[tool.poetry.dev-dependencies]
black = "^20.8b1"
-32
View File
@@ -21,33 +21,6 @@ from gql.transport.websockets import WebsocketsTransport
class SpeckleClient:
"""
The `SpeckleClient` is your entry point for interacting with your Speckle Server's GraphQL API.
You'll need to have access to a server to use it, or you can use our public server `speckle.xyz`.
To authenticate the client, you'll need to have downloaded the [Speckle Manager](https://speckle.guide/#speckle-manager)
and added your account.
```py
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with a token (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate(token=account.token)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
# use that stream id to get the stream from the server
new_stream = client.stream.get(id=new_stream_id)
```
"""
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
@@ -82,11 +55,6 @@ class SpeckleClient:
except Exception as ex:
raise SpeckleException(f"{self.url} is not a compatible Speckle Server", ex)
def __repr__(self):
return (
f"SpeckleClient( server: {self.url}, authenticated: {self.me is not None} )"
)
def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL entrypoint is created
+2 -107
View File
@@ -1,14 +1,9 @@
import os
from warnings import warn
from pydantic import BaseModel
from typing import List, Optional
from urllib.parse import urlparse, unquote
from specklepy.logging import metrics
from pydantic import BaseModel
from specklepy.api.models import ServerInfo
from specklepy.api.client import SpeckleClient
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.transports.server.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.logging.exceptions import SpeckleException
class UserInfo(BaseModel):
@@ -42,7 +37,6 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
Returns:
List[Account] -- list of all local accounts or an empty list if no accounts were found
"""
metrics.track(metrics.ACCOUNT_LIST)
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
json_path = os.path.join(account_storage._base_path, "Accounts")
os.makedirs(json_path, exist_ok=True)
@@ -75,7 +69,6 @@ def get_default_account(base_path: str = None) -> Account:
Returns:
Account -- the default account or None if no local accounts were found
"""
metrics.track(metrics.ACCOUNT_DEFAULT)
accounts = get_local_accounts(base_path=base_path)
if not accounts:
return None
@@ -86,101 +79,3 @@ def get_default_account(base_path: str = None) -> Account:
default.isDefault = True
return default
class StreamWrapper:
stream_url: str = None
use_ssl: bool = True
host: str = None
stream_id: str = None
commit_id: str = None
object_id: str = None
branch_name: str = None
client: SpeckleClient = None
account: Account = None
def __repr__(self):
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
def __str__(self) -> str:
return self.__repr__()
@property
def type(self) -> str:
if self.object_id:
return "object"
elif self.commit_id:
return "commit"
elif self.branch_name:
return "branch"
else:
return "stream" if self.stream_id else "invalid"
def __init__(self, url: str) -> None:
metrics.track("streamwrapper")
self.stream_url = url
parsed = urlparse(url)
self.host = parsed.netloc
self.use_ssl = parsed.scheme == "https"
segments = parsed.path.strip("/").split("/")
if not segments or len(segments) > 4 or len(segments) < 2:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
while segments:
segment = segments.pop(0)
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
def get_account(self) -> Account:
if self.account:
return self.account
self.account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
None,
)
return self.account
def get_client(self) -> SpeckleClient:
if self.client:
return self.client
if not self.account:
self.get_account()
self.client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if self.account is None:
warn(f"No local account found for server {self.host}", SpeckleWarning)
return self.client
self.client.authenticate(self.account.token)
return self.client
def get_transport(self) -> ServerTransport:
if not self.client:
self.get_client()
return ServerTransport(self.client, self.stream_id)
-1
View File
@@ -22,7 +22,6 @@ class Commit(BaseModel):
authorName: Optional[str]
authorId: Optional[str]
authorAvatar: Optional[str]
branchName: Optional[str]
createdAt: Optional[str]
sourceApplication: Optional[str]
referencedObject: Optional[str]
+7 -11
View File
@@ -1,7 +1,8 @@
import json
from typing import List
from specklepy.logging import metrics
from specklepy.objects.base import Base
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.transports.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
@@ -22,7 +23,6 @@ def send(
Returns:
str -- the object id of the sent object
"""
metrics.track(metrics.RECEIVE)
if not transports and not use_default_cache:
raise SpeckleException(
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
@@ -52,13 +52,12 @@ def receive(
Arguments:
obj_id {str} -- the id of the object to receive
remote_transport {Transport} -- the transport to receive from
local_transport {Transport} -- the local cache to check for existing objects
(defaults to `SQLiteTransport`)
local_transport {Transport} -- the transport to send from
Returns:
Base -- the base object
"""
metrics.track(metrics.RECEIVE)
if not local_transport:
local_transport = SQLiteTransport()
@@ -93,13 +92,14 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
Returns:
str -- the serialized object
"""
metrics.track(metrics.SERIALIZE)
serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1]
def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Base:
def deserialize(
obj_string: str, read_transport: AbstractTransport = SQLiteTransport()
) -> Base:
"""
Deserialize a string object into a Base object. If the object contains referenced child objects that are not stored in the local db, a read transport needs to be provided in order to recompose the base with the children objects.
@@ -111,10 +111,6 @@ def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Ba
Returns:
Base -- the deserialized object
"""
metrics.track(metrics.DESERIALIZE)
if not read_transport:
read_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
+3 -8
View File
@@ -1,10 +1,9 @@
from specklepy.transports.sqlite import SQLiteTransport
from logging import error
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from typing import Dict, List
from gql.client import Client
from gql.gql import gql
from gql.transport.exceptions import TransportQueryError
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
class ResourceBase(object):
@@ -41,11 +40,7 @@ class ResourceBase(object):
if schema:
return schema.parse_obj(response)
elif self.schema:
try:
return self.schema.parse_obj(response)
except:
s = BaseObjectSerializer(read_transport=SQLiteTransport())
return s.recompose_base(response)
return self.schema.parse_obj(response)
else:
return response
+1 -1
View File
@@ -1,5 +1,6 @@
from typing import Optional, List
from gql import gql
from pydantic.main import BaseModel
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Commit
@@ -39,7 +40,6 @@ class Resource(ResourceBase):
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
+2 -10
View File
@@ -1,9 +1,7 @@
from gql import gql
from typing import Dict, List, Optional
from specklepy.logging import metrics
from specklepy.api.models import Stream
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Stream
NAME = "stream"
METHODS = [
@@ -37,7 +35,6 @@ class Resource(ResourceBase):
Returns:
Stream -- the retrieved stream
"""
metrics.track(metrics.STREAM_GET)
query = gql(
"""
query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) {
@@ -93,7 +90,6 @@ class Resource(ResourceBase):
Returns:
List[Stream] -- A list of Stream objects
"""
metrics.track(metrics.STREAM_LIST)
query = gql(
"""
query User($stream_limit: Int!) {
@@ -151,7 +147,6 @@ class Resource(ResourceBase):
Returns:
id {str} -- the id of the newly created stream
"""
metrics.track(metrics.STREAM_CREATE)
query = gql(
"""
mutation StreamCreate($stream: StreamCreateInput!) {
@@ -182,7 +177,6 @@ class Resource(ResourceBase):
Returns:
bool -- whether the stream update was successful
"""
metrics.track(metrics.STREAM_UPDATE)
query = gql(
"""
mutation StreamUpdate($stream: StreamUpdateInput!) {
@@ -213,7 +207,6 @@ class Resource(ResourceBase):
Returns:
bool -- whether the deletion was successful
"""
metrics.track(metrics.STREAM_DELETE)
query = gql(
"""
mutation StreamDelete($id: String!) {
@@ -246,7 +239,6 @@ class Resource(ResourceBase):
Returns:
List[Stream] -- a list of Streams that match the search query
"""
metrics.track(metrics.STREAM_SEARCH)
query = gql(
"""
query StreamSearch($search_query: String!,$limit: Int!, $branch_limit:Int!, $commit_limit:Int!) {
-5
View File
@@ -28,8 +28,3 @@ class GraphQLException(SpeckleException):
def __str__(self) -> str:
return f"GraphQLException: {self.message}"
class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None:
super().__init__(*args)
-125
View File
@@ -1,125 +0,0 @@
import os
import queue
import logging
import requests
import threading
from requests.sessions import session
from specklepy.transports.sqlite import SQLiteTransport
"""
Anonymous telemetry to help us understand how to make a better Speckle.
This really helps us to deliver a better open source project and product!
"""
TRACK = True
HOST_APP = "python"
LOG = logging.getLogger(__name__)
METRICS_TRACKER = None
# actions
RECEIVE = "receive"
SEND = "send"
STREAM_CREATE = "stream/create"
STREAM_GET = "stream/get"
STREAM_UPDATE = "stream/update"
STREAM_DELETE = "stream/delete"
STREAM_DETAILS = "stream/details"
STREAM_LIST = "stream/list"
STREAM_VIEW = "stream/view"
STREAM_SEARCH = "stream/search"
ACCOUNT_DEFAULT = "account/default"
ACCOUNT_DETAILS = "account/details"
ACCOUNT_LIST = "account/list"
SERIALIZE = "serialization/serialize"
DESERIALIZE = "serialization/deserialize"
def disable():
global TRACK
TRACK = False
def set_host_app(host_app: str):
global HOST_APP
HOST_APP = host_app
def track(action: str):
if not TRACK:
return
try:
global METRICS_TRACKER
if not METRICS_TRACKER:
METRICS_TRACKER = MetricsTracker()
page_params = {
"rec": 1,
"idsite": METRICS_TRACKER.site_id,
"uid": METRICS_TRACKER.suuid,
"action_name": action,
"url": f"http://connectors/{HOST_APP}/{action}",
"urlref": f"http://connectors/{HOST_APP}/{action}",
"_cvar": {"1": ["hostApplication", HOST_APP]},
}
event_params = {
"rec": 1,
"idsite": METRICS_TRACKER.site_id,
"uid": MetricsTracker.suuid,
"_cvar": {"1": ["hostApplication", HOST_APP]},
"e_c": HOST_APP,
"e_a": action,
}
METRICS_TRACKER.queue.put_nowait([event_params, page_params])
except Exception as ex:
# wrapping this whole thing in a try except as we never want a failure here to annoy users!
LOG.error("Error queueing metrics request: " + str(ex))
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class MetricsTracker(metaclass=Singleton):
matomo_url = "https://speckle.matomo.cloud/matomo.php"
site_id = 2
host_app = "python"
suuid = None
sending_thread = None
queue = queue.Queue(1000)
def __init__(self) -> None:
self.sending_thread = threading.Thread(
target=self._send_tracking_requests, daemon=True
)
self.set_suuid()
self.sending_thread.start()
def set_suuid(self):
try:
file_path = os.path.join(SQLiteTransport.get_base_path("Speckle"), "suuid")
with open(file_path, "r") as file:
self.suuid = file.read()
except:
self.suuid = "unknown-suuid"
def _send_tracking_requests(self):
session = requests.Session()
while True:
params = self.queue.get()
try:
session.post(self.matomo_url, params=params[0])
session.post(self.matomo_url, params=params[1])
except Exception as ex:
LOG.error("Error sending metrics request: " + str(ex))
self.queue.task_done()
+72 -206
View File
@@ -1,73 +1,15 @@
import typing
from typing import (Any, Callable, ClassVar, Dict, List, Optional, Set, Type,
get_type_hints)
from warnings import warn
from inspect import getattr_static
from pydantic import BaseModel, validator
from pydantic.main import Extra
from typing import ClassVar, Dict, List, Optional, Any, Set, Type
from specklepy.transports.memory import MemoryTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.units import get_units_from_string
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
# to remove from dir() when calling get_member_names()
REMOVE_FROM_DIR = {
"Config",
"_Base__dict_helper",
"__annotations__",
"__class__",
"__delattr__",
"__dict__",
"__dir__",
"__doc__",
"__eq__",
"__format__",
"__ge__",
"__getattribute__",
"__getitem__",
"__gt__",
"__hash__",
"__init__",
"__init_subclass__",
"__le__",
"__lt__",
"__module__",
"__ne__",
"__new__",
"__reduce__",
"__reduce_ex__",
"__repr__",
"__setattr__",
"__setitem__",
"__sizeof__",
"__str__",
"__subclasshook__",
"__weakref__",
"_chunk_size_default",
"_chunkable",
"_count_descendants",
"_attr_types",
"_detachable",
"_handle_object_count",
"_type_check",
"_type_registry",
"_units",
"add_chunkable_attrs",
"add_detachable_attrs",
"get_children_count",
"get_dynamic_member_names",
"get_id",
"get_member_names",
"get_registered_type",
"get_typed_member_names",
"to_dict",
"update_forward_refs",
"validate_prop_name",
"from_list",
"to_list",
}
class _RegisteringBase:
class _RegisteringBase(BaseModel):
"""
Private Base model for Speckle types.
@@ -79,8 +21,7 @@ class _RegisteringBase:
"""
speckle_type: ClassVar[str]
_type_registry: ClassVar[Dict[str, "Base"]] = {}
_attr_types: ClassVar[Dict[str, Type]] = {}
_type_registry: ClassVar[Dict[str, Type["Base"]]] = {}
class Config:
validate_assignment = True
@@ -92,10 +33,7 @@ class _RegisteringBase:
def __init_subclass__(
cls,
speckle_type: str = None,
chunkable: Dict[str, int] = None,
detachable: Set[str] = None,
serialize_ignore: Set[str] = None,
speckle_type: Optional[str] = None,
**kwargs: Dict[str, Any],
):
"""
@@ -113,19 +51,6 @@ class _RegisteringBase:
)
cls.speckle_type = speckle_type or cls.__name__
cls._type_registry[cls.speckle_type] = cls # type: ignore
try:
cls._attr_types = get_type_hints(cls)
except Exception:
cls._attr_types = getattr(cls, "__annotations__", {})
if chunkable:
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)
super().__init_subclass__(**kwargs)
@@ -134,16 +59,9 @@ class Base(_RegisteringBase):
totalChildrenCount: Optional[int] = None
applicationId: Optional[str] = None
_units: str = "m"
# dict of chunkable props and their max chunk size
_chunkable: Dict[str, int] = {}
_chunkable: Dict[str, int] = {} # dict of chunkable props and their max chunk size
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
_serialize_ignore: Set[str] = set()
def __init__(self, **kwargs) -> None:
super().__init__()
for k, v in kwargs.items():
self.__setattr__(k, v)
def __repr__(self) -> str:
return (
@@ -155,23 +73,6 @@ class Base(_RegisteringBase):
def __str__(self) -> str:
return self.__repr__()
@classmethod
def of_type(cls, speckle_type: str, **kwargs) -> "Base":
"""
Get a plain Base object with a specified speckle_type.
The speckle_type is protected and cannot be overwritten on a class instance.
This is to prevent problems with receiving in other platforms or connectors.
However, if you really need a base with a different type, here is a helper
to do that for you.
This is used in the deserialisation of unknown types so their speckle_type
can be preserved.
"""
b = cls(**kwargs)
b.__dict__.update(speckle_type=speckle_type)
return b
def __setitem__(self, name: str, value: Any) -> None:
self.validate_prop_name(name)
self.__dict__[name] = value
@@ -181,49 +82,24 @@ class Base(_RegisteringBase):
def __setattr__(self, name: str, value: Any) -> None:
"""
Type checking, guard attribute, and property set mechanism.
Guard attribute and property set mechanism.
The `speckle_type` is a protected class attribute it must not be overridden.
This also performs a type check if the attribute is type hinted.
"""
if name == "speckle_type":
# not sure if we should raise an exception here??
# raise SpeckleException(
# "Cannot override the `speckle_type`. This is set manually by the class or on deserialisation"
# )
return
# if value is not None:
value = self._type_check(name, value)
attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
try:
attr.__set__(self, value)
except AttributeError:
pass # the prop probably doesn't have a setter
super().__setattr__(name, value)
@classmethod
def update_forward_refs(cls) -> None:
"""
Attempts to populate the internal defined types dict for type checking sometime after defining the class.
This is already done when defining the class, but can be called again if references to undefined types were
included.
See `objects.geometry` for an example of how this is used with the Brep class definitions
"""
try:
cls._attr_types = get_type_hints(cls)
except Exception as e:
warn(
f"Could not update forward refs for class {cls.__name__}: {e}")
if name != "speckle_type":
attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
try:
attr.__set__(self, value)
except AttributeError:
pass # the prop probably doesn't have a setter
super().__setattr__(name, value)
@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")
if name in ("", "@"):
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 '@'",
@@ -233,45 +109,6 @@ class Base(_RegisteringBase):
"Invalid Name: Base member names cannot contain characters '.' or '/'",
)
def _type_check(self, name: str, value: Any):
"""
Lightweight type checking of values before setting them
NOTE: Does not check subscripted types within generics as the performance hit of checking
each item within a given collection isn't worth it. Eg if you have a type Dict[str, float],
we will only check if the value you're trying to set is a dict.
"""
types = getattr(self, "_attr_types", {})
t = types.get(name, None)
if t is None:
return value
if t.__module__ == "typing":
origin = getattr(t, "__origin__")
t = 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."
)
return value
if isinstance(value, t):
return value
# 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:
return float(value)
except ValueError:
pass
if t is str and value is not None:
return str(value)
raise SpeckleException(
f"Cannot set '{self.__class__.__name__}.{name}': it expects type '{t.__name__}', but received type '{type(value).__name__}'"
)
def add_chunkable_attrs(self, **kwargs: int) -> None:
"""
Mark defined attributes as chunkable for serialisation
@@ -299,26 +136,53 @@ class Base(_RegisteringBase):
def units(self, value: str):
self._units = get_units_from_string(value)
def to_dict(self) -> Dict[str, Any]:
"""Convenience method to view the whole base object as a dict"""
base_dict = self.__dict__
for key, value in base_dict.items():
if not value or isinstance(value, PRIMITIVES):
continue
else:
base_dict[key] = self.__dict_helper(value)
return base_dict
def __dict_helper(self, obj: Any) -> Any:
if not obj or isinstance(obj, PRIMITIVES):
return obj
if isinstance(obj, Base):
return self.__dict_helper(obj.__dict__)
if isinstance(obj, (list, set)):
return [self.__dict_helper(v) for v in obj]
if not isinstance(obj, dict):
raise SpeckleException(
message=f"Could not convert to dict due to unrecognized type: {type(obj)}"
)
for k, v in obj.items():
if v and not isinstance(obj, PRIMITIVES):
obj[k] = self.__dict_helper(v)
return obj
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
attr_dir = list(set(dir(self)) - REMOVE_FROM_DIR)
return [
attrs = list(self.__dict__.keys())
properties = [
name
for name in attr_dir
if not name.startswith("_") and not callable(getattr(self, name))
for name in dir(self)
if not name.startswith("_")
and name
!= "fields" # soon to be removed as this pydantic prop is depreciated
and isinstance(getattr(self, name, None), property)
]
def get_serializable_attributes(self) -> List[str]:
"""Get the attributes that should be serialized"""
return list(set(self.get_member_names()) - self._serialize_ignore)
return attrs + properties
def get_typed_member_names(self) -> List[str]:
"""Get all of the names of the defined (typed) properties of this object"""
return list(self._attr_types.keys())
return list(self.__fields__.keys())
def get_dynamic_member_names(self) -> List[str]:
"""Get all of the names of the dynamic properties of this object"""
return list(set(self.__dict__.keys()) - set(self._attr_types.keys()))
return list(set(self.__dict__.keys()) - set(self.__fields__.keys()))
def get_children_count(self) -> int:
"""Get the total count of children Base objects"""
@@ -337,8 +201,9 @@ 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:
@@ -350,11 +215,15 @@ class Base(_RegisteringBase):
return 0
parsed.append(base)
return sum(
self._handle_object_count(value, parsed)
for name, value in base.get_member_names()
if not name.startswith("@")
)
count = 0
for name, value in base.__dict__.items():
if name.startswith("@"):
continue
else:
count += self._handle_object_count(value, parsed)
return count
def _handle_object_count(self, obj: Any, parsed: List) -> int:
count = 0
@@ -380,12 +249,9 @@ class Base(_RegisteringBase):
count += self._handle_object_count(value, parsed)
return count
Base.update_forward_refs()
class Config:
extra = Extra.allow
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: List[Any] = None
def __init__(self) -> None:
self.data = []
data: List[Any] = []
-127
View File
@@ -1,127 +0,0 @@
from enum import Enum
from typing import Any, Callable, List, Type
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
class CurveTypeEncoding(int, Enum):
Arc = 0
Circle = 1
Curve = 2
Ellipse = 3
Line = 4
Polyline = 5
Polycurve = 6
@property
def object_class(self) -> Type:
from . import geometry
if self == self.Arc:
return geometry.Arc
elif self == self.Circle:
return geometry.Circle
elif self == self.Curve:
return geometry.Curve
elif self == self.Ellipse:
return geometry.Ellipse
elif self == self.Line:
return geometry.Line
elif self == self.Polyline:
return geometry.Polyline
elif self == self.Polycurve:
return geometry.Polycurve
raise SpeckleException(
f'No corresponding object class for CurveTypeEncoding: {self}')
def curve_from_list(args: List[float]):
curve_type = CurveTypeEncoding(args[0])
return curve_type.object_class.from_list(args)
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
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}'
)
data_chunk.encode_object(object=obj)
return data_chunk
@staticmethod
def decode_data(data: List[Any], decoder: Callable[[List[Any]], Base]) -> List[Base]:
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
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)
class CurveArray(ObjectArray):
@classmethod
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':
data = []
for curve in curves:
curve_list = curve.to_list()
curve_list.insert(0, len(curve_list))
data.extend(curve_list)
crv_array = cls()
crv_array.data = data
return crv_array
@staticmethod
def curve_from_list(args: List[float]) -> Base:
curve_type = CurveTypeEncoding(args[0])
return curve_type.object_class.from_list(args)
@property
def type(self) -> CurveTypeEncoding:
return CurveTypeEncoding(self.data[0])
def to_curve(self) -> Base:
return self.type.object_class.from_list(self.data)
@classmethod
def _curve_decoder(cls, data: List[float]) -> Base:
crv_array = cls()
crv_array.data = data
return crv_array.to_curve()
def to_curves(self) -> List[Base]:
return self.decode(decoder=self._curve_decoder)
+5 -10
View File
@@ -14,12 +14,7 @@ CHUNKABLE_PROPS = {
DETACHABLE = {"detach_this", "origin", "detached_list"}
class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
pointslist: List[Base] = None
dots: List[int] = None
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
class FakeMesh(Base):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
@@ -29,10 +24,10 @@ class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
detached_list: List[Base] = None
_origin: Point = None
# def __init__(self, **kwargs) -> None:
# super(FakeMesh, self).__init__(**kwargs)
# self.add_chunkable_attrs(**CHUNKABLE_PROPS)
# self.add_detachable_attrs(DETACHABLE)
def __init__(self, **kwargs) -> None:
super(FakeMesh, self).__init__(**kwargs)
self.add_chunkable_attrs(**CHUNKABLE_PROPS)
self.add_detachable_attrs(DETACHABLE)
@property
def origin(self):
+73 -453
View File
@@ -1,49 +1,33 @@
from enum import Enum
from typing import Any, List, Optional
from .base import Base
from .encoding import CurveArray, CurveTypeEncoding, ObjectArray
from .units import get_encoding_from_units, get_units_from_encoding
from typing import Any, List
GEOMETRY = "Objects.Geometry."
class Interval(Base, speckle_type="Objects.Primitive.Interval"):
start: float = 0.0
end: float = 0.0
start: float = 0
end: float = 0
def length(self):
return abs(self.start - self.end)
@classmethod
def from_list(cls, args: List[Any]) -> "Interval":
return cls(start=args[0], end=args[1])
def to_list(self) -> List[Any]:
return [self.start, self.end]
class Point(Base, speckle_type=GEOMETRY + "Point"):
x: float = 0.0
y: float = 0.0
z: float = 0.0
x: float = 0
y: float = 0
z: float = 0
def __init__(self, x: float = 0, y: float = 0, z: float = 0, **data: Any) -> None:
super().__init__(**data)
self.x, self.y, self.z = x, y, z
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id: {self.id}, speckle_type: {self.speckle_type})"
@classmethod
def from_list(cls, args: List[float]) -> "Point":
return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[Any]:
@property
def value(self) -> List[float]:
return [self.x, self.y, self.z]
@classmethod
def from_coords(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0):
pt = Point()
pt.x, pt.y, pt.z = x, y, z
return pt
class Vector(Point, speckle_type=GEOMETRY + "Vector"):
pass
@@ -59,23 +43,6 @@ class Plane(Base, speckle_type=GEOMETRY + "Plane"):
xdir: Vector = Vector()
ydir: Vector = Vector()
@classmethod
def from_list(cls, args: List[Any]) -> "Plane":
return cls(
origin=Point.from_list(args[0:3]),
normal=Vector.from_list(args[3:6]),
xdir=Vector.from_list(args[6:9]),
ydir=Vector.from_list(args[9:12]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.extend(self.origin.to_list())
encoded.extend(self.normal.to_list())
encoded.extend(self.xdir.to_list())
encoded.extend(self.ydir.to_list())
return encoded
class Box(Base, speckle_type=GEOMETRY + "Box"):
basePlane: Plane = Plane()
@@ -93,21 +60,6 @@ class Line(Base, speckle_type=GEOMETRY + "Line"):
bbox: Box = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Line":
return cls(
start=Point.from_list(args[0:3]),
end=Point.from_list(args[3:6]),
domain=Interval.from_list(args[6:9]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.extend(self.start.to_list())
encoded.extend(self.end.to_list())
encoded.extend(self.domain.to_list())
return encoded
class Arc(Base, speckle_type=GEOMETRY + "Arc"):
radius: float = None
@@ -123,30 +75,6 @@ class Arc(Base, speckle_type=GEOMETRY + "Arc"):
area: float = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Arc":
return cls(
radius=args[1],
startAngle=args[2],
endAngle=args[3],
angleRadians=args[4],
domain=Interval.from_list(args[5:7]),
plane=Plane.from_list(args[7:20]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Arc.value)
encoded.append(self.radius)
encoded.append(self.startAngle)
encoded.append(self.endAngle)
encoded.append(self.angleRadians)
encoded.extend(self.domain.to_list())
encoded.extend(self.plane.to_list())
encoded.append(get_encoding_from_units(self.units))
return encoded
class Circle(Base, speckle_type=GEOMETRY + "Circle"):
radius: float = None
@@ -156,24 +84,6 @@ class Circle(Base, speckle_type=GEOMETRY + "Circle"):
area: float = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Circle":
return cls(
radius=args[1],
domain=Interval.from_list(args[2:4]),
plane=Plane.from_list(args[4:17]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Circle.value)
encoded.append(self.radius),
encoded.extend(self.domain.to_list())
encoded.extend(self.plane.to_list())
encoded.append(get_encoding_from_units(self.units))
return encoded
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
firstRadius: float = None
@@ -185,28 +95,8 @@ class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
area: float = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Ellipse":
return cls(
firstRadius=args[1],
secondRadius=args[2],
domain=Interval.from_list(args[3:5]),
plane=Plane.from_list(args[5:18]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Ellipse.value)
encoded.append(self.firstRadius)
encoded.append(self.secondRadius)
encoded.extend(self.domain.to_list())
encoded.extend(self.plane.to_list())
encoded.append(get_encoding_from_units(self.units))
return encoded
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
class Polyline(Base, speckle_type=GEOMETRY + "Polyline"):
value: List[float] = None
closed: bool = None
domain: Interval = None
@@ -214,34 +104,27 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
area: float = None
length: float = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_chunkable_attrs(value=20000)
@classmethod
def from_points(cls, points: List[Point]):
polyline = cls()
polyline.units = points[0].units
polyline.value = []
for point in points:
polyline.value.extend([point.x, point.y, point.z])
return polyline
@classmethod
def from_list(cls, args: List[Any]) -> "Polyline":
point_count = args[4]
return cls(
closed=bool(args[1]),
domain=Interval.from_list(args[2:4]),
value=args[5 : 5 + point_count],
units=get_units_from_encoding(args[-1]),
)
# @property
# def value(self) -> List[float]:
# return self._value
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Polyline.value)
encoded.append(int(self.closed))
encoded.extend(self.domain.to_list())
encoded.append(len(self.value))
encoded.extend(self.value)
encoded.append(get_encoding_from_units(self.units))
return encoded
# @value.setter
# def value(self, coords) -> None:
# if len(coords) % 3:
# coords.extend([0] * (3 - len(coords) % 3))
# self._value = coords
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
@@ -252,16 +135,10 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.value)
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
return [Point(v, next(values), next(values), units=self.units) for v in values]
class Curve(
Base,
speckle_type=GEOMETRY + "Curve",
chunkable={"points": 20000, "weights": 20000, "knots": 20000},
):
class Curve(Base, speckle_type=GEOMETRY + "Curve"):
degree: int = None
periodic: bool = None
rational: bool = None
@@ -275,6 +152,10 @@ class Curve(
area: float = None
length: float = None
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_chunkable_attrs(points=20000, weights=20000, knots=20000)
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.points:
@@ -284,80 +165,17 @@ class Curve(
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.points)
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
@classmethod
def from_list(cls, args: List[Any]) -> "Curve":
point_count = args[7]
weights_count = args[8]
knots_count = args[9]
points_start = 10
weights_start = 10 + point_count
knots_start = weights_start + weights_count
knots_end = knots_start + knots_count
return cls(
degree=args[1],
periodic=bool(args[2]),
rational=bool(args[3]),
closed=bool(args[4]),
domain=Interval.from_list(args[5:7]),
points=args[points_start:weights_start],
weights=args[weights_start:knots_start],
knots=args[knots_start:knots_end],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Curve.value)
encoded.append(self.degree)
encoded.append(int(self.periodic))
encoded.append(int(self.rational))
encoded.append(int(self.closed))
encoded.extend(self.domain.to_list())
encoded.append(len(self.points))
encoded.append(len(self.weights))
encoded.append(len(self.knots))
encoded.extend(self.points)
encoded.extend(self.weights)
encoded.extend(self.knots)
encoded.append(get_encoding_from_units(self.units))
return encoded
return [Point(v, next(values), next(values), units=self.units) for v in values]
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: List[Base] = None
segments: List[Base] = []
domain: Interval = None
closed: bool = None
bbox: Box = None
area: float = None
length: float = None
@classmethod
def from_list(cls, args: List[Any]) -> "Polycurve":
curve_arrays = CurveArray()
curve_arrays.data = args[4:-1]
return cls(
closed=bool(args[1]),
domain=Interval.from_list(args[2:4]),
segments=curve_arrays.to_curves(),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(CurveTypeEncoding.Polycurve.value)
encoded.append(int(self.closed))
encoded.extend(self.domain.to_list())
curve_array = CurveArray.from_curves(self.segments)
encoded.extend(curve_array.data)
encoded.append(get_encoding_from_units(self.units))
return encoded
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
capped: bool = None
@@ -373,16 +191,7 @@ class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
bbox: Box = None
class Mesh(
Base,
speckle_type=GEOMETRY + "Mesh",
chunkable={
"vertices": 2000,
"faces": 2000,
"colors": 2000,
"textureCoordinates": 2000,
},
):
class Mesh(Base, speckle_type=GEOMETRY + "Mesh"):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
@@ -391,6 +200,12 @@ class Mesh(
area: float = None
volume: float = None
def __init__(self, **data) -> None:
super().__init__(**data)
self.add_chunkable_attrs(
vertices=2000, faces=2000, colors=2000, textureCoordinates=2000
)
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: int = None
@@ -401,58 +216,6 @@ class Surface(Base, speckle_type=GEOMETRY + "Surface"):
countU: int = None
countV: int = None
bbox: Box = None
closedU: bool = None
closedV: bool = None
domainU: Interval = None
domainV: Interval = None
knotsU: List[float] = None
knotsV: List[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Surface":
point_count = int(args[11])
knots_u_count = int(args[12])
knots_v_count = int(args[13])
start_point_data = 14
start_knots_u = start_point_data + point_count
start_knots_v = start_knots_u + knots_u_count
return cls(
degreeU=int(args[0]),
degreeV=int(args[1]),
countU=int(args[2]),
countV=int(args[3]),
rational=bool(args[4]),
closedU=bool(args[5]),
closedV=bool(args[6]),
domainU=Interval(start=args[7], end=args[8]),
domainV=Interval(start=args[9], end=args[10]),
pointData=args[start_point_data:start_knots_u],
knotsU=args[start_knots_u:start_knots_v],
knotsV=args[start_knots_v : start_knots_v + knots_v_count],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(self.degreeU)
encoded.append(self.degreeV)
encoded.append(self.countU)
encoded.append(self.countV)
encoded.append(int(self.rational))
encoded.append(int(self.closedU))
encoded.append(int(self.closedV))
encoded.extend(self.domainU.to_list())
encoded.extend(self.domainV.to_list())
encoded.append(len(self.pointData))
encoded.append(len(self.knotsU))
encoded.append(len(self.knotsV))
encoded.extend(self.pointData)
encoded.extend(self.knotsU)
encoded.extend(self.knotsV)
encoded.append(get_encoding_from_units(self.units))
return encoded
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
@@ -472,8 +235,7 @@ class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
@property
def _loops(self):
if self.LoopIndices:
return [self._Brep.Loops[i] for i in self.LoopIndices]
return [self._Brep.Loops[index] for index in self.LoopIndices]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
@@ -495,8 +257,7 @@ class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
@property
def _trims(self):
if self.TrimIndices:
return [self._Brep.Trims[i] for i in self.TrimIndices]
return [self._Brep.Trims[i] for i in self.TrimIndices]
@property
def _curve(self):
@@ -515,19 +276,7 @@ class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
@property
def _trims(self):
if self.TrimIndices:
return [self._Brep.Trims[i] for i in self.TrimIndices]
class BrepTrimTypeEnum(int, Enum):
Unknown = 0
Boundary = 1
Mated = 2
Seam = 3
Singular = 4
CurveOnSurface = 5
PointOnSurface = 6
Slit = 7
return [self._Brep.Trims[i] for i in self.TrimIndices]
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
@@ -559,174 +308,45 @@ class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
def _curve_2d(self):
return self._Brep.Curve2D[self.CurveIndex]
@classmethod
def from_list(cls, args: List[Any]) -> "BrepTrim":
return cls(
EdgeIndex=args[0],
StartIndex=args[1],
EndIndex=args[2],
FaceIndex=args[3],
LoopIndex=args[4],
CurveIndex=args[5],
IsoStatus=args[6],
TrimType=BrepTrimTypeEnum(args[7]).name,
IsReversed=bool(args[8]),
)
def to_list(self) -> List[Any]:
encoded = []
encoded.append(self.EdgeIndex)
encoded.append(self.StartIndex)
encoded.append(self.EndIndex)
encoded.append(self.FaceIndex)
encoded.append(self.LoopIndex)
encoded.append(self.CurveIndex)
encoded.append(self.IsoStatus)
encoded.append(getattr(BrepTrimTypeEnum, self.TrimType).value)
encoded.append(self.IsReversed)
return encoded
class Brep(
Base,
speckle_type=GEOMETRY + "Brep",
chunkable={
"SurfacesValue": 200,
"Curve3DValues": 200,
"Curve2DValues": 200,
"VerticesValue": 5000,
"Edges": 5000,
"Loops": 5000,
"TrimsValue": 5000,
"Faces": 5000,
},
detachable={"displayValue"},
serialize_ignore={"Surfaces", "Curve3D", "Curve2D", "Vertices", "Trims"},
):
class Brep(Base, speckle_type=GEOMETRY + "Brep"):
provenance: str = None
bbox: Box = None
area: float = None
volume: float = None
displayValue: Mesh = None
Surfaces: List[Surface] = None
Curve3D: List[Base] = None
Curve2D: List[Base] = None
Vertices: List[Point] = None
Surfaces: List[Surface] = []
Curve3D: List[Base] = []
Curve2D: List[Base] = []
Vertices: List[Point] = []
Edges: List[BrepEdge] = []
Loops: List[BrepLoop] = []
Trims: List[BrepTrim] = []
Faces: List[BrepFace] = []
IsClosed: bool = None
Orientation: int = None
Orientation: int = 0
def _inject_self_into_children(self, children: Optional[List[Base]]) -> List[Base]:
if children is None:
return children
def __init__(self, **data: Any) -> None:
super().__init__(**data)
self.add_detachable_attrs({"displayValue"})
self.add_chunkable_attrs(
Surfaces=200,
Curve3D=200,
Curve2D=200,
Vertices=5000,
Edges=5000,
Loops=5000,
Trims=5000,
Faces=5000,
)
for child in children:
child._Brep = self
return children
@property
def Edges(self) -> List[BrepEdge]:
return self._inject_self_into_children(self._Edges)
@Edges.setter
def Edges(self, value: List[BrepEdge]):
self._Edges = value
@property
def Loops(self) -> List[BrepLoop]:
return self._inject_self_into_children(self._Loops)
@Loops.setter
def Loops(self, value: List[BrepLoop]):
self._Loops = value
@property
def Faces(self) -> List[BrepFace]:
return self._inject_self_into_children(self._Faces)
@Faces.setter
def Faces(self, value: List[BrepFace]):
self._Faces = value
@property
def SurfacesValue(self) -> List[float]:
if self.Surfaces is None:
return None
return ObjectArray.from_objects(self.Surfaces).data
@SurfacesValue.setter
def SurfacesValue(self, value: List[float]):
self.Surfaces = ObjectArray.decode_data(value, Surface.from_list)
@property
def Curve3DValues(self) -> List[float]:
if self.Curve3D is None:
return None
return CurveArray.from_curves(self.Curve3D).data
@Curve3DValues.setter
def Curve3DValues(self, value: List[float]):
crv_array = CurveArray()
crv_array.data = value
self.Curve3D = crv_array.to_curves()
@property
def Curve2DValues(self) -> List[Base]:
if self.Curve2D is None:
return None
return CurveArray.from_curves(self.Curve2D).data
@Curve2DValues.setter
def Curve2DValues(self, value: List[float]):
crv_array = CurveArray()
crv_array.data = value
self.Curve2D = crv_array.to_curves()
@property
def VerticesValue(self) -> List[Point]:
if self.Vertices is None:
return None
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
values = [encoded_unit]
for vertex in self.Vertices:
values.extend(vertex.to_list())
return values
@VerticesValue.setter
def VerticesValue(self, value: List[float]):
value = value.copy()
units = get_units_from_encoding(value.pop(0))
vertices = []
for i in range(0, len(value), 3):
vertex = Point.from_list(value[i : i + 3])
vertex._units = units
vertices.append(vertex)
self.Vertices = vertices
@property
def Trims(self) -> List[BrepTrim]:
return self._inject_self_into_children(self._Trims)
@Trims.setter
def Trims(self, value: List[BrepTrim]):
self._Trims = value
@property
def TrimsValue(self) -> List[float]:
if self.Trims is None:
return None
values = []
for trim in self.Trims:
values.extend(trim.to_list())
return values
@TrimsValue.setter
def TrimsValue(self, value: List[float]):
self.Trims = [
BrepTrim.from_list(value[i : i + 9]) for i in range(0, len(value), 9)
]
def __setattr__(self, name: str, value: Any) -> None:
if not value:
return
if name in ["Edges", "Loops", "Trims", "Faces"]:
for val in value:
val._Brep = self
super().__setattr__(name, value)
BrepEdge.update_forward_refs()
-31
View File
@@ -6,7 +6,6 @@ UNITS_STRINGS = {
"mm": ["mm", "mil", "millimeters", "millimetres"],
"cm": ["cm", "centimetre", "centimeter", "centimetres", "centimeters"],
"m": ["m", "meter", "meters", "metre", "metres"],
"km": ["km", "kilometer", "kilometre", "kilometers", "kilometres"],
"in": ["in", "inch", "inches"],
"ft": ["ft", "foot", "feet"],
"yd": ["yd", "yard", "yards"],
@@ -14,17 +13,6 @@ UNITS_STRINGS = {
"none": ["none", "null"],
}
UNITS_ENCODINGS = {
"mm": 1,
"cm": 2,
"m": 3,
"km": 4,
"in": 5,
"ft": 6,
"yd": 7,
"mi": 8,
}
def get_units_from_string(unit: str):
unit = str.lower(unit)
@@ -35,22 +23,3 @@ def get_units_from_string(unit: str):
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit (eg {UNITS})."
)
def get_units_from_encoding(unit: int):
for name, encoding in UNITS_ENCODINGS.items():
if unit == encoding:
return name
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit encoding (eg {UNITS_ENCODINGS})."
)
def get_encoding_from_units(unit: str):
try:
return UNITS_ENCODINGS[unit]
except KeyError:
raise SpeckleException(
message=f"No encoding exists for unit {unit}. Please enter a valid unit to encode (eg {UNITS_ENCODINGS})."
)
@@ -1,4 +1,4 @@
import ujson
import json
import hashlib
import re
@@ -14,7 +14,7 @@ PRIMITIVES = (int, float, str, bool)
def hash_obj(obj: Any) -> str:
return hashlib.sha256(ujson.dumps(obj).encode()).hexdigest()[:32]
return hashlib.sha256(json.dumps(obj).encode()).hexdigest()[:32]
class BaseObjectSerializer:
@@ -35,7 +35,7 @@ class BaseObjectSerializer:
self.__reset_writer()
self.detach_lineage = [True]
hash, obj = self.traverse_base(base)
return hash, ujson.dumps(obj)
return hash, json.dumps(obj)
def traverse_base(self, base: Base) -> Tuple[str, Dict]:
"""Decomposes the given base object and builds a serializable dictionary
@@ -52,7 +52,7 @@ class BaseObjectSerializer:
self.lineage.append(uuid4().hex)
object_builder = {"id": "", "speckle_type": "Base", "totalChildrenCount": 0}
object_builder.update(speckle_type=base.speckle_type)
obj, props = base, base.get_serializable_attributes()
obj, props = base, base.get_member_names()
while props:
prop = props.pop(0)
@@ -70,9 +70,7 @@ class BaseObjectSerializer:
# only bother with chunking and detaching if there is a write transport
if self.write_transports:
dynamic_chunk_match = prop.startswith("@") and re.match(
r"^@\((\d*)\)", prop
)
dynamic_chunk_match = re.match(r"^@\((\d*)\)", prop)
if dynamic_chunk_match:
chunk_size = dynamic_chunk_match.groups()[0]
base._chunkable[prop] = (
@@ -142,7 +140,7 @@ class BaseObjectSerializer:
# write detached or root objects to transports
if detached and self.write_transports:
for t in self.write_transports:
t.save_object(id=hash, serialized_object=ujson.dumps(object_builder))
t.save_object(id=hash, serialized_object=json.dumps(object_builder))
del self.lineage[-1]
@@ -238,7 +236,7 @@ class BaseObjectSerializer:
"""
if not obj_string:
return None
obj = ujson.loads(obj_string)
obj = json.loads(obj_string)
return self.recompose_base(obj=obj)
def recompose_base(self, obj: dict) -> Base:
@@ -254,7 +252,7 @@ class BaseObjectSerializer:
if not obj:
return
if isinstance(obj, str):
obj = ujson.loads(obj)
obj = json.loads(obj)
if "speckle_type" in obj and obj["speckle_type"] == "reference":
obj = self.get_child(obj=obj)
@@ -268,7 +266,7 @@ class BaseObjectSerializer:
object_type = Base.get_registered_type(speckle_type)
# initialise the base object using `speckle_type` fall back to base if needed
base = object_type() if object_type else Base.of_type(speckle_type=speckle_type)
base = object_type() if object_type else Base(speckle_type=speckle_type)
# get total children count
if "__closure" in obj:
if not self.read_transport:
@@ -292,7 +290,7 @@ class BaseObjectSerializer:
raise SpeckleException(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
ref_obj = ujson.loads(ref_obj_str)
ref_obj = json.loads(ref_obj_str)
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
# 3. handle all other cases (base objects, lists, and dicts)
@@ -326,7 +324,7 @@ class BaseObjectSerializer:
# handle chunked lists
data = []
for o in obj_list:
data.extend(o.data)
data.extend(o["data"])
return data
return obj_list
@@ -350,4 +348,4 @@ class BaseObjectSerializer:
raise SpeckleException(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}"
)
return ujson.loads(ref_obj_str)
return json.loads(ref_obj_str)
+11 -19
View File
@@ -102,9 +102,7 @@ class BatchSender(object):
new_objects = [obj[1] for obj in batch if obj[0] in new_object_ids]
if not new_objects:
LOG.info(
f"Uploading batch of {len(batch)} objects: all objects are already in the server"
)
LOG.info(f"Uploading batch of {len(batch)} objects: all objects are already in the server")
return
upload_data = "[" + ",".join(new_objects) + "]"
@@ -114,30 +112,24 @@ class BatchSender(object):
% (len(batch), len(new_objects), len(upload_data), len(upload_data_gzip))
)
try:
r = session.post(
url=f"{self.server_url}/objects/{self.stream_id}",
files={"batch-1": ("batch-1", upload_data_gzip, "application/gzip")},
)
if r.status_code != 201:
LOG.warning("Upload server response: %s", r.text)
raise SpeckleException(
message=f"Could not save the object to the server - status code {r.status_code}"
)
except json.JSONDecodeError as error:
return SpeckleException(
f"Failed to send objects to {self.server_url}. Please ensure this stream ({self.stream_id}) exists on this server and that you have permission to send to it.",
error,
r = session.post(
url=f"{self.server_url}/objects/{self.stream_id}",
files={"batch-1": ("batch-1", upload_data_gzip, "application/gzip")},
)
if r.status_code != 201:
LOG.warning("Upload server response: %s", r.text)
raise SpeckleException(
message=f"Could not save the object to the server - status code {r.status_code}"
)
def _create_threads(self):
for _ in range(self.thread_count):
for i in range(self.thread_count):
t = threading.Thread(target=self._sending_thread_main, daemon=True)
t.start()
self._send_threads.append(t)
def _delete_threads(self):
for _ in range(len(self._send_threads)):
for i in range(len(self._send_threads)):
self._batches.put(None)
for thread in self._send_threads:
+5 -15
View File
@@ -22,15 +22,11 @@ class ServerTransport(AbstractTransport):
def __init__(self, client: SpeckleClient, stream_id: str, **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
token = client.me["token"]
self._batch_sender = BatchSender(
self.url, self.stream_id, token, max_batch_size_mb=1
)
self._batch_sender = BatchSender(self.url, self.stream_id, token, max_batch_size_mb=1)
self.session = requests.Session()
self.session.headers.update(
@@ -77,25 +73,19 @@ class ServerTransport(AbstractTransport):
r.encoding = "utf-8"
if r.status_code != 200:
raise SpeckleException(
f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})"
)
raise SpeckleException(f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})")
root_obj_serialized = r.text
root_obj = json.loads(root_obj_serialized)
closures = root_obj.get("__closure", {})
closures = root_obj.get('__closure', {})
# Check which children are not already in the target transport
children_ids = list(closures.keys())
children_found_map = target_transport.has_objects(children_ids)
new_children_ids = [
id for id in children_found_map if not children_found_map[id]
]
new_children_ids = [id for id in children_found_map if not children_found_map[id]]
# Get the new children
endpoint = f"{self.url}/api/getobjects/{self.stream_id}"
r = self.session.post(
endpoint, data={"objects": json.dumps(new_children_ids)}, stream=True
)
r = self.session.post(endpoint, data={"objects": json.dumps(new_children_ids)}, stream=True)
if r.encoding is None:
r.encoding = "utf-8"
lines = r.iter_lines(decode_unicode=True)
+30 -34
View File
@@ -6,6 +6,7 @@ import sqlite3
from typing import Any, List, Dict
from appdirs import user_data_dir
from contextlib import closing
from multiprocessing import Process, Queue
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.logging.exceptions import SpeckleException
@@ -18,6 +19,7 @@ class SQLiteTransport(AbstractTransport):
_scheduler = sched.scheduler(time.time, time.sleep)
_polling_interval = 0.5 # seconds
__connection: sqlite3.Connection = None
__queue: Queue = Queue()
app_name: str = ""
scope: str = ""
saved_obj_count: int = 0
@@ -32,20 +34,14 @@ class SQLiteTransport(AbstractTransport):
super().__init__(**data)
self.app_name = app_name or "Speckle"
self.scope = scope or "Objects"
self._base_path = base_path or self.get_base_path(self.app_name)
self._base_path = base_path or self.__get_base_path()
try:
os.makedirs(self._base_path, exist_ok=True)
os.makedirs(self._base_path, exist_ok=True)
self._root_path = os.path.join(
os.path.join(self._base_path, f"{self.scope}.db")
)
self.__initialise()
except Exception as ex:
raise SpeckleException(
f"SQLiteTransport could not initialise {self.scope}.db at {self._base_path}. Either provide a different `base_path` or use an alternative transport.",
ex,
)
self._root_path = os.path.join(
os.path.join(self._base_path, f"{self.scope}.db")
)
self.__initialise()
def __repr__(self) -> str:
return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')"
@@ -56,8 +52,7 @@ class SQLiteTransport(AbstractTransport):
# proc.start()
# proc.join()
@staticmethod
def get_base_path(app_name):
def __get_base_path(self):
# from appdirs https://github.com/ActiveState/appdirs/blob/master/appdirs.py
# default mac path is not the one we use (we use unix path), so using special case for this
system = sys.platform
@@ -68,26 +63,26 @@ class SQLiteTransport(AbstractTransport):
if os_name.startswith("Mac"):
system = "darwin"
if system != "darwin":
return user_data_dir(appname=app_name, appauthor=False, roaming=True)
if system == "darwin":
path = os.path.expanduser("~/.config/")
return os.path.join(path, self.app_name)
else:
return user_data_dir(appname=self.app_name, appauthor=False, roaming=True)
path = os.path.expanduser("~/.config/")
return os.path.join(path, app_name)
def __consume_queue(self):
if self._is_writing or self.__queue.empty():
return
print("CONSUME QUEUE")
self._is_writing = True
while not self.__queue.empty():
data = self.__queue.get()
self.save_object(data[0], data[1])
self._is_writing = False
# def __consume_queue(self):
# if self._is_writing or self.__queue.empty():
# return
# print("CONSUME QUEUE")
# self._is_writing = True
# while not self.__queue.empty():
# data = self.__queue.get()
# self.save_object(data[0], data[1])
# self._is_writing = False
# self._scheduler.enter(
# delay=self._polling_interval, priority=1, action=self.__consume_queue
# )
# self._scheduler.run(blocking=True)
self._scheduler.enter(
delay=self._polling_interval, priority=1, action=self.__consume_queue
)
self._scheduler.run(blocking=True)
# def save_object(self, id: str, serialized_object: str) -> None:
# """Adds an object to the queue and schedules it to be saved.
@@ -107,14 +102,15 @@ class SQLiteTransport(AbstractTransport):
def save_object_from_transport(
self, id: str, source_transport: AbstractTransport
) -> None:
"""Adds an object from the given transport to the the local db
"""Adds an object from the given transport to the queue and schedules it to be saved.
Arguments:
id {str} -- the object id
source_transport {AbstractTransport) -- the transport through which the object can be found
"""
serialized_object = source_transport.get_object(id)
self.save_object(id, serialized_object)
self.__queue.put((id, serialized_object))
raise NotImplementedError
def save_object(self, id: str, serialized_object: str) -> None:
"""Directly saves an object into the database.
+1 -5
View File
@@ -7,9 +7,6 @@ from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.objects.geometry import Point
from specklepy.objects.fakemesh import FakeMesh
from specklepy.logging import metrics
metrics.disable()
@pytest.fixture(scope="session")
@@ -93,7 +90,7 @@ def mesh():
[1, 2, 3],
Base(name="detached within a list"),
]
mesh.origin = Point(x=4, y=2)
mesh.origin = Point(value=[4, 2, 0])
return mesh
@@ -105,5 +102,4 @@ def base():
base.vertices = [random.uniform(0, 10) for _ in range(1, 120)]
base.test_bases = [Base(name=i) for i in range(1, 22)]
base["@detach"] = Base(name="detached base")
base["@revit_thing"] = Base.of_type("SpecialRevitFamily", name="secret tho")
return base
+2 -43
View File
@@ -1,10 +1,7 @@
from contextlib import ExitStack as does_not_raise
from typing import Dict, List
import pytest
from specklepy.objects import Base
from specklepy.api import operations
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base, DataChunk
from contextlib import ExitStack as does_not_raise
@pytest.mark.parametrize(
@@ -75,41 +72,3 @@ def test_speckle_type_cannot_be_set(base: Base) -> None:
assert base.speckle_type == "Base"
base.speckle_type = "unset"
assert base.speckle_type == "Base"
def test_base_of_custom_speckle_type() -> None:
b1 = Base.of_type("BirdHouse", name="Tweety's Crib")
assert b1.speckle_type == "BirdHouse"
assert b1.name == "Tweety's Crib"
class FrozenYoghurt(Base):
"""Testing type checking"""
servings: int
flavours: List[str] = None # list item types won't be checked
customer: str
add_ons: Dict[str, float] # dict item types won't be checked
price: float = 0.0
def test_type_checking() -> None:
order = FrozenYoghurt()
order.servings = 2
order.price = "7" # will get converted
order.customer = "izzy"
with pytest.raises(SpeckleException):
order.flavours = "not a list"
with pytest.raises(SpeckleException):
order.servings = "five"
with pytest.raises(SpeckleException):
order.add_ons = ["sprinkles"]
order.add_ons = {"sprinkles": 0.2, "chocolate": 1.0}
order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
assert order.price == 7.0
-389
View File
@@ -1,389 +0,0 @@
import json
from typing import Callable
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.transports.memory import MemoryTransport
@pytest.fixture()
def interval():
return Interval(start=0, end=5)
@pytest.fixture()
def point():
return Point(x=1, y=10, z=0)
@pytest.fixture()
def vector():
return Vector(x=1, y=32, z=10)
@pytest.fixture()
def plane(point, vector):
return Plane(
origin=point,
normal=vector,
xdir=vector,
ydir=vector,
)
@pytest.fixture()
def box(plane, interval):
return Box(
basePlane=plane,
ySize=interval,
zSize=interval,
xSize=interval,
area=20.4,
volume=44.2,
)
@pytest.fixture()
def line(point, interval):
return Line(
start=point,
end=point,
domain=interval,
# These attributes are not handled in C#
# bbox=None,
# length=None
)
@pytest.fixture()
def arc(plane, interval):
return Arc(
radius=2.3,
startAngle=22.1,
endAngle=44.5,
angleRadians=33,
plane=plane,
domain=interval,
units='m',
# These attributes are not handled in C#
# bbox=None,
# area=None,
# length=None,
# startPoint=None,
# midPoint=None,
# endPoint=None,
)
@pytest.fixture()
def circle(plane, interval):
return Circle(
radius=22,
plane=plane,
domain=interval,
units='m',
# These attributes are not handled in C#
# bbox=None,
# area=None,
# length=None,
)
@pytest.fixture()
def ellipse(plane, interval):
return Ellipse(
firstRadius=34,
secondRadius=22,
plane=plane,
domain=interval,
units='m',
# These attributes are not handled in C#
# trimDomain=None,
# bbox=None,
# area=None,
# length=None,
)
@pytest.fixture()
def polyline(interval):
return Polyline(
value=[22, 44, 54.3, 99, 232, 21],
closed=True,
domain=interval,
units='m',
# These attributes are not handled in C#
# bbox=None,
# area=None,
# length=None,
)
@pytest.fixture()
def curve(interval):
return Curve(
degree=90,
periodic=True,
rational=False,
closed=True,
domain=interval,
points=[23, 21, 44, 43, 56, 76, 1, 3, 2],
weights=[23, 11, 23],
knots=[22, 45, 76, 11],
units='m',
# These attributes are not handled in C#
# displayValue=None,
# bbox=None,
# area=None,
# length=None,
)
@pytest.fixture()
def polycurve(interval, curve, polyline):
return Polycurve(
segments=[curve, polyline],
domain=interval,
closed=True,
units='m',
# These attributes are not handled in C#
# bbox=None,
# area=None,
# length=None
)
@pytest.fixture()
def mesh(box):
return Mesh(
vertices=[2, 1, 2, 4, 77.3, 5, 33, 4, 2],
faces=[1, 2, 3, 4, 5, 6, 7],
colors=[111, 222, 333, 444, 555, 666, 777],
bbox=box,
area=233,
volume=232.2,
)
@pytest.fixture()
def surface(interval):
return Surface(
degreeU=33,
degreeV=44,
rational=True,
pointData=[1, 2.2, 3, 4, 5, 6, 7, 8, 9],
countU=3,
countV=4,
closedU=True,
closedV=False,
domainU=interval,
domainV=interval,
knotsU=[1.1, 2.2, 3.3, 4.4],
knotsV=[9, 8, 7, 6, 5, 4.4],
units='m',
# These attributes are not handled in C#
# bbox=None,
# area=None,
)
@pytest.fixture()
def brep_face():
return BrepFace(
SurfaceIndex=3,
LoopIndices=[1, 2, 3, 4],
OuterLoopIndex=2,
OrientationReversed=False,
)
@pytest.fixture()
def brep_edge(interval):
return BrepEdge(
Curve3dIndex=2,
TrimIndices=[4, 5, 6, 7],
StartIndex=2,
EndIndex=6,
ProxyCurveIsReversed=True,
Domain=interval,
)
@pytest.fixture()
def brep_loop():
return BrepLoop(
FaceIndex=5,
TrimIndices=[3, 4, 5],
Type='unknown'
)
@pytest.fixture()
def brep_trim():
return BrepTrim(
EdgeIndex=3,
StartIndex=4,
EndIndex=6,
FaceIndex=1,
LoopIndex=4,
CurveIndex=7,
IsoStatus=6,
TrimType='Mated',
IsReversed=False,
# These attributes are not handled in C#
# Domain=None,
)
@pytest.fixture
def brep(mesh, box, surface, curve, polyline, circle, point,
brep_edge, brep_loop, brep_trim, brep_face):
return Brep(
provenance='pytest',
bbox=box,
area=32,
volume=54,
displayValue=mesh,
Surfaces=[surface, surface, surface],
Curve3D=[curve, polyline],
Curve2D=[circle],
Vertices=[point, point, point, point],
Edges=[brep_edge],
Loops=[brep_loop, brep_loop],
Trims=[brep_trim],
Faces=[brep_face, brep_face],
IsClosed=False,
Orientation=3,
)
@pytest.fixture
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
}
@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')
chunks = object.to_list()
assert isinstance(chunks, list)
object_class = object.__class__
decoded_object: Base = object_class.from_list(chunks)
assert decoded_object.get_id() == object.get_id()
def test_brep_surfaces_value_serialization(surface):
brep = Brep()
assert brep.Surfaces == None
assert brep.SurfacesValue == None
brep.Surfaces = [surface, surface]
assert brep.SurfacesValue == ObjectArray.from_objects(
[surface, surface]).data
brep.SurfacesValue = ObjectArray.from_objects([surface]).data
assert len(brep.Surfaces) == 1
assert brep.Surfaces[0].get_id() == surface.get_id()
def test_brep_curve2d_values_serialization(curve, polyline, circle):
brep = Brep()
assert brep.Curve2D == None
assert brep.Curve2DValues == None
brep.Curve2D = [curve, polyline]
assert brep.Curve2DValues == CurveArray.from_curves([curve, polyline]).data
brep.Curve2DValues = CurveArray.from_curves([circle]).data
assert len(brep.Curve2D) == 1
assert brep.Curve2D[0].get_id() == circle.get_id()
def test_brep_curve3d_values_serialization(curve, polyline, circle):
brep = Brep()
assert brep.Curve3D == None
assert brep.Curve3DValues == None
brep.Curve3D = [curve, polyline]
assert brep.Curve3DValues == CurveArray.from_curves([curve, polyline]).data
brep.Curve3DValues = CurveArray.from_curves([circle]).data
assert len(brep.Curve3D) == 1
assert brep.Curve3D[0].get_id() == circle.get_id()
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()
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,
]
brep.Trims[0].get_id() == BrepTrim(
EdgeIndex=0,
StartIndex=0,
EndIndex=0,
FaceIndex=0,
LoopIndex=0,
CurveIndex=0,
IsoStatus=1,
TrimType=BrepTrimTypeEnum.Boundary,
IsReversed=False,
).get_id()
brep.Trims[1].get_id() == BrepTrim(
EdgeIndex=1,
StartIndex=0,
EndIndex=0,
FaceIndex=0,
LoopIndex=0,
CurveIndex=1,
IsoStatus=2,
TrimType=BrepTrimTypeEnum.Boundary,
IsReversed=True,
).get_id()
def test_serialized_brep_attributes(brep: Brep):
transport = MemoryTransport()
serialized = operations.serialize(brep, [transport])
serialized_dict = json.loads(serialized)
removed_keys = ['Surfaces', 'Curve3D', 'Curve2D', 'Vertices', 'Trims']
for k in removed_keys:
assert k not in serialized_dict.keys()
+7 -23
View File
@@ -1,9 +1,8 @@
import pytest
from specklepy.api.models import Stream
from specklepy.objects import Base
from specklepy.objects.encoding import ObjectArray
from specklepy.transports.memory import MemoryTransport
from specklepy.api.models import Stream
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
import pytest
class TestObject:
@@ -20,15 +19,12 @@ class TestObject:
return stream
def test_object_create(self, client, stream, base):
transport = SQLiteTransport()
s = BaseObjectSerializer(
write_transports=[transport], read_transport=transport)
transport = MemoryTransport()
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"
assert obj_id == base.get_id(True)
def test_object_get(self, client, stream, base):
@@ -39,16 +35,4 @@ class TestObject:
assert isinstance(fetched_base, Base)
assert fetched_base.name == base.name
assert isinstance(fetched_base.vertices, list)
# assert fetched_base["@detach"]["speckle_type"] == "reference"
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
]
assert array.decode(decoder=sum) == [5, 4, 3, 2, 1]
assert fetched_base["@detach"]["speckle_type"] == "reference"
+1 -2
View File
@@ -19,7 +19,6 @@ class TestSerialization:
assert base.get_id() == deserialized.get_id()
assert base.units == "mm"
assert isinstance(base.test_bases[0], Base)
assert base["@revit_thing"].speckle_type == "SpecialRevitFamily"
assert base["@detach"].name == deserialized["@detach"].name
def test_detaching(self, mesh):
@@ -61,7 +60,7 @@ class TestSerialization:
assert isinstance(received, FakeMesh)
assert received.vertices == mesh.vertices
assert isinstance(received.origin, Point)
assert received.origin.x == mesh.origin.x
assert received.origin.value == mesh.origin.value
# not comparing hashes as order is not guaranteed back from server
mesh.id = hash # populate with decomposed id for use in proceeding tests
-41
View File
@@ -1,41 +0,0 @@
import pytest
from specklepy.api.credentials import StreamWrapper
class TestWrapper:
def test_parse_stream(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
assert wrap.type == "stream"
def test_parse_branch(self):
wacky_wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F"
)
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/next%20level"
)
assert wacky_wrap.type == "branch"
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
assert wrap.type == "branch"
def test_parse_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
)
assert wrap.type == "commit"
def test_parse_object(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6"
)
assert wrap.type == "object"
def test_parse_globals_as_branch(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/0c6ad366c4/globals/")
assert wrap.type == "branch"
def test_parse_globals_as_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893"
)
assert wrap.type == "commit"