Compare commits

...

105 Commits

Author SHA1 Message Date
Gergő Jedlicska e842f651b9 Merge pull request #253 from RobClaessensRHDHV/feature/structural_property_bugfix
Typo fix in properties.py
2023-01-16 13:53:44 +01:00
908599 7e1bec1aba Typo fix in properties.py 2023-01-16 11:39:41 +01:00
Gergő Jedlicska 1fb9a4f5fe Merge pull request #252 from specklesystems/gergo/set_type_fix
fix(typing-system): add set type into type validation
2023-01-11 16:10:28 +01:00
Gergő Jedlicska 1668c80bed fix(typing-system): add set type into type validation 2023-01-11 16:07:04 +01:00
Gergő Jedlicska ac6ba87c68 Merge pull request #251 from specklesystems/gergo/unionTriples
fix(type-validation): fix union types with more than 2 arguments
2023-01-10 13:02:36 +01:00
Gergő Jedlicska 3db8565f57 fix(type-validation): fix union types with more than 2 arguments 2023-01-10 12:58:19 +01:00
Alan Rynne a32822f4e3 ci: Updated github actions to use new actions repo 2023-01-09 20:42:24 +01:00
Gergő Jedlicska 40956927c8 Merge pull request #250 from specklesystems/gergo/nonGenericTFix
fix(typing): fix non generic typedefed lists and tuples
2023-01-09 15:54:36 +01:00
Gergő Jedlicska 4628f111ba fix(type-checking): fix py >= 3.9 dict type checking 2023-01-09 15:53:15 +01:00
Gergő Jedlicska 9c952b432d fix(typing): fix non specificed generic types for py 3.7 py 3.8 2023-01-09 15:39:45 +01:00
Gergő Jedlicska f075988e4b fix(typing): fix non generic typedefed lists and tuples 2023-01-09 15:10:23 +01:00
Gergő Jedlicska 65c829404a Merge pull request #247 from specklesystems/gergo/pathingUnification
feat(path-handling): rework path handling to match behavior of Core
2023-01-06 15:20:28 +01:00
Jedd Morgan 85e7a72524 fix(tests): Fixed regex escape on path tests 2023-01-06 14:00:19 +00:00
Gergő Jedlicska 0533aa0139 Merge pull request #248 from specklesystems/gergo/updatePypiAuth
;51R;51R
2023-01-05 16:18:22 +01:00
Gergő Jedlicska 04b733a241 ci(circleci): update pypi context usage 2023-01-05 16:15:49 +01:00
Gergő Jedlicska 01036c0f2e test(unit-tests): fix unit tests after merge 2023-01-05 11:06:11 +01:00
Gergő Jedlicska 4d8ca534fe Merge branch 'main' of github.com:specklesystems/specklepy into gergo/pathingUnification 2023-01-05 11:05:03 +01:00
Gergő Jedlicska e941efd95c refactor(tests): split into unit and integration tests folder 2023-01-05 10:59:43 +01:00
Gergő Jedlicska be9defbfa9 Merge pull request #245 from specklesystems/gergo/fully_qualified_names
feat(Base): rework speckle_type handling to better align with Core
2023-01-05 10:55:32 +01:00
Gergő Jedlicska dd54c69554 feat(path-handling): rework path handling to match behavior of Core 2023-01-05 10:41:39 +01:00
Gergő Jedlicska 93c1ec9556 Merge pull request #240 from ItsPatrickHe/bugfix/results_default_values
Update default value of results from 0.0 to None to align with the default value in speckle-sharp
2023-01-04 10:31:18 +01:00
Gergő Jedlicska 659c57e840 Merge branch 'main' of github.com:specklesystems/specklepy into pr/ItsPatrickHe/240 2023-01-04 10:27:38 +01:00
Gergő Jedlicska 1cdd4ffc9c feat(Base): rework speckle_type handling to better align with Core
use a dotnet `FullName` like behavior to deremine the speckle_type

fixes #231
2023-01-04 10:15:36 +01:00
Gergő Jedlicska 5ae022d2ed Merge pull request #244 from specklesystems/gergo/fixNoneInListSerialization
fix(BaseObjectSerializer): fix converting None in list to the string "None"
2022-12-21 16:04:33 +01:00
Gergő Jedlicska 31ca59cea8 fix(BaseObjectSerializer): fix converting None in list to the string "None" 2022-12-21 16:00:49 +01:00
Jinxuan He c91f673dba Add optional for float, regarding type checking 2022-12-21 12:38:21 +01:00
Gergő Jedlicska e2c8ef1b3d Merge pull request #239 from mortenengen/update-pydantic-dep
fix: update minimum required version of pydantic
2022-12-21 12:05:44 +01:00
Gergő Jedlicska b6b0a5a3a0 Merge pull request #242 from specklesystems/gergo/pre-commit
gergo/pre commit
2022-12-21 12:00:45 +01:00
Gergő Jedlicska f36d63a2cf Merge pull request #241 from specklesystems/speckle_type_sync_with_core
speckle type sync with core
2022-12-21 11:59:39 +01:00
Gergő Jedlicska afb9065fb9 fix(metrics): fix missing default arg to track function 2022-12-20 14:53:59 +01:00
Gergő Jedlicska fcc33f8989 style(all-project): fix all linting errors across the project with the current setup 2022-12-20 14:46:36 +01:00
Gergő Jedlicska 2cf9b64221 style(server-transport): fix import export 2022-12-20 10:47:48 +01:00
Gergő Jedlicska 990cf4eb2f style(whole-project): fixing linting and typing errors 2022-12-20 10:45:22 +01:00
Gergő Jedlicska b25f2ab4bc style(whole-project): fix styling from new pre-commit config 2022-12-19 14:03:20 +01:00
Gergő Jedlicska a8786c126d build(pre-commit-config.yaml): add basic pre-commit config 2022-12-19 14:02:42 +01:00
Gergő Jedlicska a35f8936ca bump: version 2.9.1 → 2.9.2 2022-12-19 12:44:11 +01:00
Gergő Jedlicska d965cc0988 add pre-commit dependency 2022-12-19 12:37:20 +01:00
Gergő Jedlicska 59a6950eed fix type checking error message 2022-12-16 22:01:56 +01:00
Gergő Jedlicska 6804282fac import ordering 2022-12-16 21:45:47 +01:00
Gergő Jedlicska a3a22a43d5 formatting fixes 2022-12-16 21:45:24 +01:00
Gergő Jedlicska 08aaa41b6c fix units 2022-12-16 21:45:04 +01:00
Gergő Jedlicska 07a3213ee9 fix objects import 2022-12-16 21:21:13 +01:00
Gergő Jedlicska 7a46176803 additional test cases for type validation 2022-12-16 21:17:39 +01:00
Jinxuan He b75501addd Update default value from 0.0 to None to align them with the default value in speckle-sharp 2022-12-16 15:42:59 +01:00
Gergő Jedlicska 15636dbe62 rework type checking in base 2022-12-16 13:41:07 +01:00
Gergő Jedlicska 9a2061d900 add missing object definitions from core 2022-12-16 13:40:43 +01:00
Morten Engen 8dc51fb936 Update min required version of pydantic 2022-12-16 12:30:56 +01:00
Gergő Jedlicska e805b8ac6e style(all): sort imports with isort 2022-12-09 20:48:02 +01:00
Gergő Jedlicska 5a1d624979 refactor(objects): fix import structure and typing errors for objects 2022-12-09 20:47:16 +01:00
Gergő Jedlicska c8808b07b3 fix(objects): update structural objects to better match core 2022-12-09 19:46:48 +01:00
Gergő Jedlicska 1a9b847c44 test(base): update base tests to properly test the speckle_type generation 2022-12-09 19:46:09 +01:00
Gergő Jedlicska f01fcb8e66 fix(base): update speckle_type creation to better match Core 2022-12-09 19:45:20 +01:00
Gergő Jedlicska 67499ab20c Merge pull request #219 from specklesystems/gergo/typeFixes
typing and formatting fixes
2022-12-09 12:36:07 +01:00
Gergő Jedlicska ebb703e68d Merge branch 'main' of github.com:specklesystems/specklepy into gergo/typeFixes 2022-12-08 14:27:42 +01:00
Gergő Jedlicska ef5d41da5d Merge pull request #233 from ItsPatrickHe/bugfix/property_attr_fix
Align the attribute names for Property1D, Property2D and Property3D w…
2022-12-08 14:26:36 +01:00
Gergő Jedlicska 851dd9c482 Merge pull request #234 from specklesystems/gergo/ciFix
ci(circleci): force integration test server to use localhost ipv4 adresses
2022-12-08 14:01:07 +01:00
Gergő Jedlicska 78074cf691 ci(circleci): force integration test server to use localhost ipv4 adresses 2022-12-08 13:03:39 +01:00
Jinxuan He be0daa419d Align the attribute names for Property1D, Property2D and Property3D with the speckle-sharp repo 2022-12-05 14:32:59 +01:00
Gergő Jedlicska a7d1b9ce30 Merge pull request #230 from specklesystems/gergo/py311
testing cimg/python311
2022-11-02 12:43:49 +01:00
Gergő Jedlicska 05756a8e9e testing cimg/python311 2022-11-02 12:41:32 +01:00
Gergő Jedlicska b50e658333 Merge pull request #228 from specklesystems/gergo/noDiskAccess
make sure specklepy send works completely without disk write access
2022-11-01 20:50:14 +01:00
Gergő Jedlicska 88248353ab make sure specklepy send works completely without disk write access 2022-11-01 16:43:17 +01:00
Alan Rynne aec94f8f7f Merge pull request #225 from specklesystems/gergo/limited_user_query
deprecate user resoure, add new active_user and other_user resources
2022-10-28 10:06:12 +02:00
Gergő Jedlicska e6b1604bc3 fix seed user redirects 2022-10-26 16:45:24 +02:00
Gergő Jedlicska de29b93b8b fix test ordering 2022-10-26 14:49:12 +02:00
Gergő Jedlicska 10aa8b59b6 update lock file 2022-10-26 14:43:22 +02:00
Gergő Jedlicska b86faa6a14 remove pytest sugar, its broken on circleci 2022-10-26 14:34:23 +02:00
Gergő Jedlicska 7430611c52 remove old dev dependency syntax 2022-10-26 14:31:04 +02:00
Gergő Jedlicska ddd52f4af9 deprecate user resoure, add new active_user and other_user resources 2022-10-26 11:30:04 +02:00
Gergő Jedlicska 35bc6b0350 Merge pull request #223 from specklesystems/gergo/src_folder
move code to /src/ folder pattern
2022-10-25 13:11:29 +03:00
Gergő Jedlicska 9585d46c4e move code to /src/ folder pattern 2022-10-25 11:55:27 +02:00
Gergő Jedlicska 1900fece8b version bump 2022-10-07 16:02:16 +02:00
Gergő Jedlicska 344b7de557 typing and formatting fixes 2022-10-07 15:59:48 +02:00
Alan Rynne fd09e97a53 Merge pull request #218 from specklesystems/gergo/receiveMetricsUpdate
gergo/receiveMetricsUpdate
2022-10-06 17:24:32 +02:00
Gergő Jedlicska 459bd0901f add untracked receive operations for connector side tracking 2022-10-06 16:40:03 +02:00
Gergő Jedlicska ae7c4bc14d add host aplication implementation from sharp 2022-10-06 16:39:40 +02:00
Gergő Jedlicska 41f1823aae Merge pull request #215 from specklesystems/gergo/urlCompare
gergo/urlCompare
2022-09-27 15:19:09 +02:00
Gergő Jedlicska 625bd5cd84 fixing path.unlink for py37 2022-09-27 15:13:29 +02:00
Gergő Jedlicska 8812985c67 Merge branch 'main' of github.com:specklesystems/specklepy into gergo/urlCompare 2022-09-27 14:57:48 +02:00
Gergő Jedlicska c838835a65 Merge pull request #213 from specklesystems/gergo/noneUnits
gergo/noneUnits
2022-09-27 14:56:54 +02:00
Gergő Jedlicska 361ba6bfcd fix stream wrapper matching on accounts with subdomain url-s 2022-09-26 20:15:45 +02:00
Gergő Jedlicska 8078a4b596 remove literal import 2022-09-26 09:59:42 +02:00
Gergő Jedlicska 08b106464f remove unused literal 2022-09-24 16:24:48 +02:00
Gergő Jedlicska 0cfe5db674 reformat stuff, fix backwards compatible typing 2022-09-24 16:22:04 +02:00
Gergő Jedlicska d9dbca2c68 py311 is not yet supported by circleci base images 2022-09-24 16:14:43 +02:00
Gergő Jedlicska ab5b55871b test for python311 2022-09-24 16:12:01 +02:00
Gergő Jedlicska 2f87956154 remove checking for the fetched users email 2022-09-24 16:09:47 +02:00
Gergő Jedlicska 6fc4ab1539 update tests 2022-09-23 17:09:25 +02:00
Gergő Jedlicska 7f432e768d Fix warnings about None type units being set on Base objects
Add proper units enum implementation

Co-authored-by: Alan Rynne <alan@speckle.systems>
Co-authored-by: Morten Engen <morten.engen@multiconsult.no>
2022-09-23 17:08:53 +02:00
izzy lyseggen d0eb364b3e feat(client): add monitoring headers to client setup (#209)
* chore: delete unused file

* feat(client): update monitoring headers in setup
2022-09-08 10:08:34 +01:00
Reynold Chan 936b2d8b5a Update material.py 2022-08-22 23:16:19 -04:00
luzpaz 5a9aec80bd Fix: misc. typos (#208)
Just some minor typos
2022-08-22 12:24:11 +01:00
izzy lyseggen 6616526279 fix(metrics): log ex as debug for jupyter notebook (#206)
* fix(metrics): log ex as debug for jupyter notebook

so apparently in jupyter notebooks this will actually throw
unlike in a regular python environment.

changing severity to debug since no one should ever care except me
when i'm debugging lol!

* fix(metrics): another log level change
2022-08-16 10:44:14 +01:00
izzy lyseggen 6e00de58b9 Revert "fix(metrics): log ex as debug for jupyter notebook (#204)" (#205)
This reverts commit 506aaf68ca.
2022-08-16 10:41:03 +01:00
Iain Sproat 0ec404bbec GitHub template update (#203)
* Initial commit

* Create CODE_OF_CONDUCT.md

* Create CONTRIBUTING.MD

* Update CODE_OF_CONDUCT.md

adds authoritative source notice to this repo

* Create ISSUE_TEMPLATE.md

* Update CODE_OF_CONDUCT.md

* Update and rename CONTRIBUTING.MD to CONTRIBUTING.md

* Update README.md

adds basic default social badges - discourse and twitter

* Update README.md

* Update README.md

* Update README.md

* Update README.md

* Create LICENSE

* Update README.md

* Update README.md

* Update CONTRIBUTING.md

fixes link

* Update CODE_OF_CONDUCT.md

* docs: add slack link and badge

* Update README.md

* fix: link and typos

* fix: more links

* refactor: moved files to .github/ folder

* feat: added PR template

Updated docs to reflect it.

* fix: updated old link

* fix: added yaml frontmatter block to issue template

* docs: removes links to slack

* docs: adds link to docs

* Update README.md

* Create open-issue.yml

* Create close-issue.yml

* Fixes: PR template updated to provide detailed instructions

* Add link to speckle-server contribution guide

* Fix link to relative to the repo pull requests

* Feature: separates issue template into bugs and feature requests

* Provides checklist for both issue templates
* Hides instructions in comments

* Add link to contribution guidelines

* Retain some sections from previous issue template

* checklist is clearer

* style: tidy newlines and other small formatting

* Add a SECURITY.md file

* Refer to the code of conduct in the contributing section of the README

* chore(pr_template): adds a reference section to the PR template

The SpecklePY PR template had a reference section, and it made sense to include it for all
repositories.

* Remove redundant issue template

* fix(pull request template): pR template should be the default and not an option

PR template was in a directory which allows selection using queries.  The PR template should be
provided by default so should be renamed and placed in the .github directory.

Co-authored-by: Dimitrie Stefanescu <didimitrie@gmail.com>
Co-authored-by: izzy lyseggen <izzy.lyseggen@gmail.com>
Co-authored-by: Matteo Cominetti <matteo@cominetti.org>
Co-authored-by: Alan Rynne <alan@speckle.systems>
Co-authored-by: Alan Rynne <alan@rynne.es>
Co-authored-by: Matteo Cominetti <matteo@speckle.systems>
2022-08-16 10:37:21 +01:00
izzy lyseggen 506aaf68ca fix(metrics): log ex as debug for jupyter notebook (#204)
so apparently in jupyter notebooks this will actually throw
unlike in a regular python environment.

changing severity to debug since no one should ever care except me
when i'm debugging lol!
2022-08-16 10:35:57 +01:00
izzy lyseggen 9fa6b661eb feat(client): stream invites (#202)
* chore: update dev container

* feat(client): add server version request

* docs(client): enhanced server version docstring

* test(client): sever version test

* test(base): type check for union type

* refactor(base): move _ attrs to `RegisteringBase`

* refactor: some light cleanup

* chore(container): add pylint line length

* feat(metrics): add "Invite Action"

* feat(client): pass server version to resources

* feat(models): pending stream collaborator model

* fix(client): do not parse if request res is null

* feat(exceptions): add unsupported exception

to distinguish for new invites as of server 2.6.4

* feat(client): add stream invite queries/mutations

* test(client): test stream invites

* feat(client): incorporate last invites changes
2022-07-26 17:58:46 +01:00
izzy lyseggen d1adccba00 feat(serialization): cache bases on deserialize (#200)
* feat(serialization): cache bases on deserialize

allows the deserializer to return the same instance when encountering
objects with the same id

closes #191

* feat(serialization): cache bases on deserialize

allows the deserializer to return the same instance when encountering
objects with the same id

closes #191

* fix(serialization): check before accessing id

obj may not be detached and therefore might not have an id

* feat(serializer): cache w id from obj not base

* chore: update deps

* feat(operations): lil send helper

on send, if `transports` is just a transport, add it to a list.
i see this error a lot so just a friendly lil fix!

* feat(objects): breps omg!

* test(geometry): updates for breps

* test(geometry): more brep serialization tests

* refactor(test): formatting

* style: formatting

* test(geometry): clean up test file

* fix(test): brep trims test fix

* refactor(geometry): clean up encoding outputs

* feat(objects): allow kwargs in encoding

* feat(objects): align curve encodings w sharp

* test(geometry): new curve encodings

* feat(client): update stream permission mutation

guess this changed some time recently and i wasn't made aware of it

* fix(objects): brep face and edge encoding

* fix(geometry): breps units 'none' fix

* test(objects): fix 'none' units issue

* revert(486ea99): use `streamGrantPermission`

to be updated for next server release
2022-07-20 18:02:25 +01:00
Reynold Chan 9e30250446 Create test_structural.py (#180) 2022-07-15 16:38:55 +01:00
Morten Engen 3e7d657e2e feat(base): sort serializable attributes (#198)
Sorts the serializable attributes before they are returned from
.get_serializable_attributes(). Prevents the object id to change if the
order of the member names from .get_member_names() changes.
2022-07-11 12:19:22 +01:00
izzy lyseggen 7484d8441b fix(credentials): clean up sqlite transport directly after getting accts (#199)
* fix(sqlite): use `self.close()` on del

* fix(credentials): close sqlite directly after use

* refactor(serializer): consolidate var name for ids
2022-07-11 10:31:19 +01:00
izzy lyseggen 682afce05f fix(transport): call begin/end write in server (#195)
* fix(transport): call begin/end write in server

* style: remove unused code
2022-06-30 16:16:05 +01:00
izzy lyseggen 21b27e2f3b feat(metrics): add ids for unknown users (#194)
* feat(metrics): add `merge_ids` helper method

* fix(metrics): use alias ids instead

* fix(metrics): final cleanup for aliasing

* fix(metrics): lol jk scratch the aliasing
2022-06-28 12:23:19 +01:00
izzy lyseggen 69cd9706cf fix(base): remove default units to Base (#193)
* fix(base): remove default units to `Base`

change in sharp that wasn't propagated to py!

* fix(objects): add `None` to unit encodings
2022-06-22 17:03:50 +01:00
izzy lyseggen 98075fa2cf fix(metrics): remove unused prop (#192) 2022-06-22 14:52:49 +01:00
114 changed files with 7146 additions and 4057 deletions
+6 -5
View File
@@ -18,15 +18,15 @@ jobs:
- image: "speckle/speckle-server"
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "localhost"
POSTGRES_URL: "127.0.0.1"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle2_test"
REDIS_URL: "redis://localhost"
REDIS_URL: "redis://127.0.0.1"
SESSION_SECRET: "keyboard cat"
STRATEGY_LOCAL: "true"
CANONICAL_URL: "http://localhost:3000"
WAIT_HOSTS: localhost:5432, localhost:6379
WAIT_HOSTS: 127.0.0.1:5432, 127.0.0.1:6379
DISABLE_FILE_UPLOADS: "true"
parameters:
tag:
@@ -57,7 +57,7 @@ jobs:
- checkout
- run: python patch_version.py $CIRCLE_TAG
- run: poetry build
- run: poetry publish -u specklesystems -p $PYPI_PASSWORD
- run: poetry publish -u __token__ -p $PYPI_TOKEN
workflows:
main:
@@ -65,11 +65,12 @@ workflows:
- test:
matrix:
parameters:
tag: ["3.7", "3.8", "3.9", "3.10"]
tag: ["3.7", "3.8", "3.9", "3.10", "3.11"]
filters:
tags:
only: /.*/
- deploy:
context: pypi
requires:
- test
filters:
+4 -4
View File
@@ -1,11 +1,11 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.191.1/containers/python-3/.devcontainer/base.Dockerfile
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6
ARG VARIANT="3.9"
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
ARG VARIANT="3.10"
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
ARG NODE_VERSION="16"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
+4 -1
View File
@@ -22,6 +22,9 @@
"python.languageServer": "Pylance",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.linting.pylintArgs": [
"--max-line-length=120"
],
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
@@ -49,4 +52,4 @@
"postCreateCommand": "poetry config virtualenvs.create false && poetry install",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
}
+2 -8
View File
@@ -6,16 +6,12 @@ services:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
# ports:
# - "5432:5432"
network_mode: host
redis:
image: cimg/redis:6.2
# ports:
# - "6379:6379"
network_mode: host
speckle-server:
image: speckle/speckle-server
image: speckle/speckle-server:latest
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "localhost"
@@ -28,8 +24,6 @@ services:
CANONICAL_URL: "http://localhost:3000"
WAIT_HOSTS: localhost:5432, localhost:6379
DISABLE_FILE_UPLOADS: "true"
# ports:
# - "3000:3000"
network_mode: host
specklepy:
@@ -47,4 +41,4 @@ services:
command: /bin/sh -c "while sleep 1000; do :; done"
network_mode: host
# networks:
# default:
# default:
+1 -1
View File
@@ -1,3 +1,3 @@
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf
-25
View File
@@ -1,25 +0,0 @@
---
name: New issue
about: Create a report to help us improve
title:
labels:
assignees:
---
If it's your first time here - or you forgot about them - make sure you read the [contribution guidelines](CONTRIBUTING.md), and then feel free to delete this line!
### Expected vs. Actual Behavior
Describe the problem here.
### Reproduction Steps & System Config (win, osx, web, etc.)
Let us know how we can reproduce this, and attach relevant files (if any).
### Proposed Solution (if any)
Let us know what how you would solve this.
#### Optional: Affected Projects
Does this issue propagate to other dependencies or dependents? If so, list them here!
+113
View File
@@ -0,0 +1,113 @@
---
name: Bug report
about: Help improve Speckle!
title: ''
labels: bug
assignees: ''
---
<!---
Provide a short summary in the Title above. Examples of good Issue titles:
* "Bug: Error from server when reticulating splines"
* "Bug: Revit crashes when installing connector"
-->
## Prerequisites
<!---
Please answer the following questions before submitting an issue.
-->
- [ ] I read the [contribution guidelines](https://github.com/specklesystems/speckle-server/blob/main/CONTRIBUTING.md)
- [ ] I checked the [documentation](https://speckle.guide/) and found no answer.
- [ ] I checked [existing issues](../issues?q=is%3Aissue) and found no similar issue. <!-- If you do find an existing issue, please show your support by liking it :+1: instead of creating a new issue -->
- [ ] I checked the [community forum](https://speckle.community/) for related discussions and found no answer.
- [ ] I'm reporting the issue to the correct repository (see also [speckle-server](https://github.com/specklesystems/speckle-server), [speckle-sharp](https://github.com/specklesystems/speckle-sharp), [specklepy](https://github.com/specklesystems/specklepy), [speckle-docs](https://github.com/specklesystems/speckle-docs), and [others](https://github.com/orgs/specklesystems/repositories))
## What package are you referring to?
<!---
Is it related to the server (backend) only, or does this bug relate to the frontend, viewer, objectloader or any other package?
-->
## Describe the bug
<!---
A clear and concise description of what the bug is.
-->
## To Reproduce
<!---
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
-->
## Expected behavior
<!---
A clear and concise description of what you expected to happen.
-->
## Screenshots
<!---
If applicable, add screenshots to help explain your problem.
-->
## System Info
If applicable, please fill in the below details - they help a lot!
### Desktop (please complete the following information):
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
### Smartphone (please complete the following information):
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
## Failure Logs
<!---
Please include any relevant log snippets or files here, or upload as a file.
If including inline, please use markdown code block syntax. https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks
For example:
```
your log output here
```
-->
## Additional context
<!---
Add any other context about the problem here.
-->
## Proposed Solution (if any)
<!---
Let us know what how you would solve this.
-->
#### Optional: Affected Projects
<!---
Does this issue propagate to other dependencies or dependents? If so, list them here with links!
-->
+71
View File
@@ -0,0 +1,71 @@
---
name: Feature request
about: Suggest an idea for Speckle!
title: ''
labels: enhancement, question
assignees: ''
---
<!---
Provide a short summary in the Title above. Examples of good Issue titles:
* "Enhancement: Connector for Minecraft"
* "Enhancement: Web viewer should support tesseracts"
-->
## Prerequisites
<!---
Please answer the following questions before submitting an issue.
-->
- [ ] I read the [contribution guidelines](https://github.com/specklesystems/speckle-server/blob/main/CONTRIBUTING.md)
- [ ] I checked the [documentation](https://speckle.guide/) and found no answer.
- [ ] I checked [existing issues](../issues?q=is%3Aissue) and found no similar issue. <!-- If you do find an existing issue, please show your support by liking it :+1: instead of creating a new issue -->
- [ ] I checked the [community forum](https://speckle.community/) for related discussions and found no answer.
- [ ] I'm requesting the feature to the correct repository (see also [speckle-server](https://github.com/specklesystems/speckle-server), [speckle-sharp](https://github.com/specklesystems/speckle-sharp), [specklepy](https://github.com/specklesystems/specklepy), [speckle-docs](https://github.com/specklesystems/speckle-docs), and [others](https://github.com/orgs/specklesystems/repositories))
## What package are you referring to?
<!---
Is it related to the server (backend) only, or does this feature request relate to the frontend, viewer, objectloader or any other package?
-->
## Is your feature request related to a problem? Please describe.
<!---
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
-->
## Describe the solution you'd like
<!---
A clear and concise description of what you want to happen.
-->
## Describe alternatives you've considered
<!---
A clear and concise description of any alternative solutions or features you've considered.
-->
## Additional context
<!---
Add any other context or screenshots about the feature request here.
Have you seen this feature implemented in any other software? Can you provide screenshots or links to video or documentation?
What works well about these existing features in other software? What doesn't work well?
-->
## Related issues or community discussions
<!---
Is this feature request related to (but sufficiently distinct from) any existing issues?
Does this feature request require other features to be available beforehand?
Has this feature been discussed in the community forum, please link here? https://speckle.community/
-->
@@ -1,25 +0,0 @@
Description of PR...
## Changes
- Item 1
- Item 2
## Checklist
- [ ] Unit tests
- [ ] Documentation
## References
(optional)
Include **important** links regarding the implementation of this PR.
This usually includes and RFC or an aggregation of issues and/or individual conversations
that helped put this solution together. This helps ensure there is a good aggregation
of resources regarding the implementation.
```text
Fixes #85, Fixes #22, Fixes username/repo#123
Connects #123
```
+102
View File
@@ -0,0 +1,102 @@
<!---
Provide a short summary in the Title above. Examples of good PR titles:
* "Feature: adds metrics to component"
* "Fix: resolves duplication in comment thread"
* "Update: apollo v2.34.0"
-->
## Description & motivation
<!---
Describe your changes, and why you're making them. What benefit will this have to others?
Is this linked to an open Github issue, a thread in Speckle community,
or another pull request? Link it here.
If it is related to a Github issue, and resolves it, please link to the issue number, e.g.:
Fixes #85, Fixes #22, Fixes username/repo#123
Connects #123
-->
## Changes:
<!---
- Item 1
- Item 2
-->
## To-do before merge:
<!---
(Optional -- remove this section if not needed)
Include any notes about things that need to happen before this PR is merged, e.g.:
- [ ] Change the base branch
- [ ] Ensure PR #56 is merged
-->
## Screenshots:
<!---
Include a screenshot the before and after. This can be a screenshot of a plugin, web frontend, or output in a terminal.
-->
## Validation of changes:
<!---
Describe what tests have been added or amended, and why these demonstrate it works and will prevent this feature being accidentally broken by future changes.
-->
## Checklist:
<!---
This checklist is mostly useful as a reminder of small things that can easily be
forgotten it is meant as a helpful tool rather than hoops to jump through.
Put an `x` between the square brackets, e.g. [x], for all the items that apply,
make notes next to any that haven't been addressed, and remove any items that are not relevant to this PR.
-->
- [ ] My pull request follows the guidelines in the [Contributing guide](https://github.com/specklesystems/speckle-server/blob/main/CONTRIBUTING.md)?
- [ ] My pull request does not duplicate any other open [Pull Requests](../../pulls) for the same update/change?
- [ ] My commits are related to the pull request and do not amend unrelated code or documentation.
- [ ] My code follows a similar style to existing code.
- [ ] I have added appropriate tests.
- [ ] I have updated or added relevant documentation.
## References
<!---
(Optional -- remove this section if not needed )
Include **important** links regarding the implementation of this PR.
This usually includes a RFC or an aggregation of issues and/or individual conversations
that helped put this solution together. This helps ensure we retain and share knowledge
regarding the implementation, and may help others understand motivation and design decisions etc..
-->
+4 -70
View File
@@ -6,73 +6,7 @@ on:
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 }}
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
+4 -42
View File
@@ -6,45 +6,7 @@ on:
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
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
+1 -1
View File
@@ -112,4 +112,4 @@ venv.bak/
# other
scratch.py
settings.json
settings.json
+33
View File
@@ -0,0 +1,33 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
hooks:
- id: ruff
rev: v0.0.186
- repo: https://github.com/commitizen-tools/commitizen
hooks:
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: v2.38.0
- repo: https://github.com/pycqa/isort
rev: v5.11.3
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
# supported by your project here, or alternatively use
# pre-commit's default_language_version, see
# https://pre-commit.com/#top_level-default_language_version
# language_version: python3.11
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+2 -2
View File
@@ -4,7 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
@@ -23,4 +23,4 @@
"justMyCode": true
}
]
}
}
+10 -4
View File
@@ -34,7 +34,7 @@ What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube
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
- [![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
@@ -47,7 +47,7 @@ Give Speckle a try in no time by:
## 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!
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.
@@ -65,6 +65,12 @@ To execute any python script run `$ poetry run python my_script.py`
> Alternatively you may roll your own virtual-env with either venv, virtualenv, pyenv-virtualenv etc. Poetry will play along an recognize if it is invoked from inside a virtual environment.
### Style guide
All our repo wide styling linting and other rules are checked and enforced by `pre-commit`, which is included in the dev dependencies.
It is recommended to set up `pre-commit` after installing the dependencies by running `$ pre-commit install`.
Commiting code that doesn't adhere to the given rules, will fail the checks in our CI system.
### Local Data Paths
It may be helpful to know where the local accounts and object cache dbs are stored. Depending on on your OS, you can find the dbs at:
@@ -74,7 +80,7 @@ It may be helpful to know where the local accounts and object cache dbs are stor
## Contributing
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) for an overview of the best practices we try to follow.
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) and [code of conduct](.github/CODE_OF_CONDUCT.md) for an overview of the practices we try to follow.
## Community
@@ -82,7 +88,7 @@ The Speckle Community hangs out on [the forum](https://discourse.speckle.works),
## Security
For any security vulnerabilities or concerns, please contact us directly at security[at]speckle.systems.
For any security vulnerabilities or concerns, please contact us directly at security[at]speckle.systems.
## License
+12
View File
@@ -0,0 +1,12 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.2.+ | :white_check_mark: |
| < 2.2 | :x: |
## Reporting a Vulnerability
Hi! If you've found something off, we'd be more than happy if you would report it via security@speckle.systems. We will work together with you to correctly identify the cause and implement a fix. Thanks for helping make Speckle safer!
+8 -8
View File
@@ -1,12 +1,13 @@
from typing import List
from specklepy.objects import Base
from specklepy.api import operations
from specklepy.transports.sqlite import SQLiteTransport
import os
import random
import string
import time
from pathlib import Path
import os
import string
import random
from typing import List
from specklepy.api import operations
from specklepy.objects import Base
from specklepy.transports.sqlite import SQLiteTransport
class Sub(Base):
@@ -26,7 +27,6 @@ def clean_db():
def one_pass(clean: bool, randomize: bool, child_count: int):
foo = Base()
for i in range(child_count):
stuff = random_string() if randomize else "stuff"
+15
View File
@@ -0,0 +1,15 @@
from devtools import debug
from specklepy.api import operations
from specklepy.api.wrapper import StreamWrapper
if __name__ == "__main__":
stream_url = "https://latest.speckle.dev/streams/7d051a6449"
wrapper = StreamWrapper(stream_url)
transport = wrapper.get_transport()
rec = operations.receive("98396753f8bf7fe1cb60c5193e9f9d86", transport)
# hash = operations.send(base=foo, transports=[transport], use_default_cache=False)
debug(rec)
+39
View File
@@ -0,0 +1,39 @@
import random
import string
from typing import List
from specklepy.api import operations
from specklepy.api.wrapper import StreamWrapper
from specklepy.objects import Base
class Sub(Base):
bar: List[str]
def random_string():
letters = string.ascii_lowercase
return "".join(random.choice(letters) for _ in range(10))
def create_object(child_count: int) -> Base:
foo = Base()
for i in range(child_count):
stuff = random_string()
foo[f"@child_{i}"] = Sub(bar=["asdf", "bar", i, stuff])
return foo
if __name__ == "__main__":
stream_url = "http://hyperion:3000/streams/2372b54c35"
child_count = 10
foo = create_object(child_count)
wrapper = StreamWrapper(stream_url)
transport = wrapper.get_transport()
hash = operations.send(base=foo, transports=[transport], use_default_cache=False)
rec = operations.receive(hash, transport)
print(rec)
+36
View File
@@ -0,0 +1,36 @@
from devtools import debug
from specklepy.api import operations
from specklepy.objects.geometry import Base
from specklepy.objects.units import Units
dct = {
"id": "1234abcd",
"units": None,
"speckle_type": "Base",
"applicationId": None,
"totalChildrenCount": 0,
}
base = Base()
for prop, value in dct.items():
base.__setattr__(prop, value)
debug(base)
debug(base.units)
base.units = "m"
debug(base.units)
base.units = None
debug(base.units)
foo = operations.serialize(base)
base.units = 10
debug(base.units)
debug(foo)
base.units = Units.mm
debug(base.units)
+3 -2
View File
@@ -1,10 +1,11 @@
"""This is an example showcasing the usage of speckle `Base` class."""
# the speckle.objects module exposes all speckle provided classes
from specklepy.objects import Base
from specklepy.api import operations
from devtools import debug
from specklepy.api import operations
from specklepy.objects import Base
class ExampleSub(Base):
"""
+1 -1
View File
@@ -9,7 +9,7 @@ def patch(tag):
lines = f.readlines()
if "version" not in lines[2]:
raise Exception(f"Invalid pyproject.toml. Could not patch version.")
raise Exception("Invalid pyproject.toml. Could not patch version.")
lines[2] = f'version = "{tag}"\n'
with open("pyproject.toml", "w") as file:
Generated
+1313 -694
View File
File diff suppressed because it is too large Load Diff
+26 -8
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.4.0"
version = "2.9.1"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -8,24 +8,35 @@ license = "Apache-2.0"
repository = "https://github.com/specklesystems/speckle-py"
documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
packages = [
{ include = "specklepy", from = "src" },
]
[tool.poetry.dependencies]
python = ">=3.7.0, <4.0"
pydantic = "^1.8.2"
python = ">=3.7.2, <4.0"
pydantic = "^1.9"
appdirs = "^1.4.4"
gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
ujson = "^5.3.0"
Deprecated = "^1.2.13"
stringcase = "^1.2.0"
[tool.poetry.dev-dependencies]
black = "^20.8b1"
[tool.poetry.group.dev.dependencies]
black = "^22.8.0"
isort = "^5.7.0"
pytest = "^6.2.2"
pytest = "^7.1.3"
pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
devtools = "^0.8.0"
pylint = "^2.14.4"
mypy = "^0.982"
pre-commit = "^2.20.0"
commitizen = "^2.38.0"
ruff = "^0.0.187"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
[tool.black]
exclude = '''
@@ -44,9 +55,16 @@ exclude = '''
'''
include = '\.pyi?$'
line-length = 88
target-version = ["py36", "py37", "py38"]
target-version = ["py37", "py38", "py39", "py310", "py311"]
[tool.commitizen]
name = "cz_conventional_commits"
version = "2.9.2"
tag_format = "$version"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.isort]
profile = "black"
-152
View File
@@ -1,152 +0,0 @@
# generated by datamodel-codegen:
# filename: stream_schema.json
# timestamp: 2020-11-17T14:33:13+00:00
from datetime import datetime
from typing import Any, Dict, List, Optional
from pydantic import BaseModel
class Collaborator(BaseModel):
id: Optional[str]
name: Optional[str]
role: Optional[str]
avatar: Optional[str]
class Commit(BaseModel):
id: Optional[str]
message: Optional[str]
authorName: Optional[str]
authorId: Optional[str]
authorAvatar: Optional[str]
branchName: Optional[str]
createdAt: Optional[datetime]
sourceApplication: Optional[str]
referencedObject: Optional[str]
totalChildrenCount: Optional[int]
parents: Optional[List[str]]
def __repr__(self) -> str:
return f"Commit( id: {self.id}, message: {self.message}, referencedObject: {self.referencedObject}, authorName: {self.authorName}, branchName: {self.branchName}, createdAt: {self.createdAt} )"
def __str__(self) -> str:
return self.__repr__()
class Commits(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Commit] = []
class Object(BaseModel):
id: Optional[str]
speckleType: Optional[str]
applicationId: Optional[str]
totalChildrenCount: Optional[int]
createdAt: Optional[datetime]
class Branch(BaseModel):
id: Optional[str]
name: Optional[str]
description: Optional[str]
commits: Optional[Commits]
class Branches(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Branch] = []
class Stream(BaseModel):
id: Optional[str]
name: Optional[str]
role: Optional[str]
isPublic: Optional[bool]
description: Optional[str]
createdAt: Optional[datetime]
updatedAt: Optional[datetime]
collaborators: List[Collaborator] = []
branches: Optional[Branches]
commit: Optional[Commit]
object: Optional[Object]
commentCount: Optional[int]
favoritedDate: Optional[datetime]
favoritesCount: Optional[int]
def __repr__(self):
return f"Stream( id: {self.id}, name: {self.name}, description: {self.description}, isPublic: {self.isPublic})"
def __str__(self) -> str:
return self.__repr__()
class Streams(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Stream] = []
class User(BaseModel):
id: Optional[str]
email: Optional[str]
name: Optional[str]
bio: Optional[str]
company: Optional[str]
avatar: Optional[str]
verified: Optional[bool]
role: Optional[str]
streams: Optional[Streams]
def __repr__(self):
return f"User( id: {self.id}, name: {self.name}, email: {self.email}, company: {self.company} )"
def __str__(self) -> str:
return self.__repr__()
class Activity(BaseModel):
actionType: Optional[str]
info: Optional[dict]
userId: Optional[str]
streamId: Optional[str]
resourceId: Optional[str]
resourceType: Optional[str]
message: Optional[str]
time: Optional[datetime]
def __repr__(self) -> str:
return f"Activity( streamId: {self.streamId}, actionType: {self.actionType}, message: {self.message}, userId: {self.userId} )"
def __str__(self) -> str:
return self.__repr__()
class ActivityCollection(BaseModel):
totalCount: Optional[int]
items: Optional[List[Activity]]
cursor: Optional[datetime]
def __repr__(self) -> str:
return f"ActivityCollection( totalCount: {self.totalCount}, items: {len(self.items) if self.items else 0}, cursor: {self.cursor.isoformat()} )"
def __str__(self) -> str:
return self.__repr__()
class ServerInfo(BaseModel):
name: Optional[str]
company: Optional[str]
url: Optional[str]
description: Optional[str]
adminContact: Optional[str]
canonicalUrl: Optional[str]
roles: Optional[List[dict]]
scopes: Optional[List[dict]]
authStrategies: Optional[List[dict]]
version: Optional[str]
-82
View File
@@ -1,82 +0,0 @@
from specklepy.api.credentials import Account
from specklepy.transports.sqlite import SQLiteTransport
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):
def __init__(
self,
account: Account,
basepath: str,
client: Client,
name: str,
methods: list,
) -> None:
self.account = account
self.basepath = basepath
self.client = client
self.name = name
self.methods = methods
self.schema = None
def _step_into_response(self, response: dict, return_type: str or List):
"""Step into the dict to get the relevant data"""
if return_type is None:
return response
elif isinstance(return_type, str):
return response[return_type]
elif isinstance(return_type, List):
for key in return_type:
response = response[key]
return response
def _parse_response(self, response: dict or list, schema=None):
"""Try to create a class instance from the response"""
if isinstance(response, list):
return [self._parse_response(response=r, schema=schema) for r in response]
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)
else:
return response
def make_request(
self,
query: gql,
params: Dict = None,
return_type: str or List = None,
schema=None,
parse_response: bool = True,
) -> Dict or GraphQLException:
"""Executes the GraphQL query"""
try:
response = self.client.execute(query, variable_values=params)
except Exception as e:
if isinstance(e, TransportQueryError):
return GraphQLException(
message=f"Failed to execute the GraphQL {self.name} request. Errors: {e.errors}",
errors=e.errors,
data=e.data,
)
else:
return SpeckleException(
message=f"Failed to execute the GraphQL {self.name} request. Inner exception: {e}",
exception=e,
)
response = self._step_into_response(response=response, return_type=return_type)
if parse_response:
return self._parse_response(response=response, schema=schema)
else:
return response
-640
View File
@@ -1,640 +0,0 @@
scalar DateTime
scalar EmailAddress
scalar BigInt
scalar JSONObject
directive @hasScope(scope: String!) on FIELD_DEFINITION
directive @hasRole(role: String!) on FIELD_DEFINITION
type Query {
"""
Stare into the void.
"""
_: String
}
type Mutation{
"""
The void stares back.
"""
_: String
}
type Subscription{
"""
It's lonely in the void.
"""
_: String
},extend type Query {
"""
Gets a specific app from the server.
"""
app( id: String! ): ServerApp
"""
Returns all the publicly available apps on this server.
"""
apps: [ServerAppListItem]
}
type ServerApp {
id: String!
secret: String!
name: String!
description: String
termsAndConditionsLink: String
logo: String
public: Boolean
trustByDefault: Boolean
author: AppAuthor
createdAt: DateTime!
redirectUrl: String!
scopes: [Scope]!
}
type ServerAppListItem {
id: String!
name: String!
description: String
termsAndConditionsLink: String
logo: String
author: AppAuthor
}
type AppAuthor {
name: String
id: String
}
extend type User {
"""
Returns the apps you have authorized.
"""
authorizedApps: [ServerAppListItem]
@hasRole(role: "server:user")
@hasScope(scope: "apps:read")
"""
Returns the apps you have created.
"""
createdApps: [ServerAppListItem]
@hasRole(role: "server:user")
@hasScope(scope: "apps:read")
}
extend type Mutation {
"""
Register a new third party application.
"""
appCreate(app: AppCreateInput!): String!
@hasRole(role: "server:user")
@hasScope(scope: "apps:write")
"""
Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.**
"""
appUpdate(app: AppUpdateInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "apps:write")
"""
Deletes a thirty party application.
"""
appDelete(appId: String!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "apps:write")
"""
Revokes (de-authorizes) an application that you have previously authorized.
"""
appRevokeAccess(appId: String!): Boolean
@hasRole(role: "server:user")
@hasScope(scope: "apps:write")
}
input AppCreateInput {
name: String!
description: String!
termsAndConditionsLink: String
logo: String
public: Boolean
redirectUrl: String!
scopes: [String]!
}
input AppUpdateInput {
id: String!
name: String!
description: String!
termsAndConditionsLink: String
logo: String
public: Boolean
redirectUrl: String!
scopes: [String]!
}
,extend type ServerInfo {
"""
The authentication strategies available on this server.
"""
authStrategies: [AuthStrategy]
}
type AuthStrategy {
id: String!,
name: String!,
icon: String!,
url: String!,
color: String
}
,extend type User{
"""
Returns a list of your personal api tokens.
"""
apiTokens: [ApiToken]
@hasRole(role: "server:user")
@hasScope(scope: "tokens:read")
}
type ApiToken {
id: String!
name: String!
lastChars: String!
scopes: [String]!
createdAt: DateTime! #date
lifespan: BigInt!
lastUsed: String! #date
}
input ApiTokenCreateInput {
scopes: [String!]!,
name: String!,
lifespan: BigInt
}
extend type Mutation {
"""
Creates an personal api token.
"""
apiTokenCreate(token: ApiTokenCreateInput!):String!
@hasRole(role: "server:user")
@hasScope(scope: "tokens:write")
"""
Revokes (deletes) an personal api token.
"""
apiTokenRevoke(token: String!):Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "tokens:write")
}
,extend type Stream {
commits(limit: Int! = 25, cursor: String): CommitCollection
commit(id: String!): Commit
branches(limit: Int! = 25, cursor: String): BranchCollection
branch(name: String!): Branch
}
extend type User {
commits(limit: Int! = 25, cursor: String): CommitCollectionUser
}
type Branch {
id: String!
name: String!
author: User!
description: String
commits(limit: Int! = 25, cursor: String): CommitCollection
}
type Commit {
id: String!
referencedObject: String!
message: String
authorName: String
authorId: String
createdAt: DateTime
}
type CommitCollectionUserNode {
id: String!
referencedObject: String!
message: String
streamId: String
streamName: String
createdAt: DateTime
}
type BranchCollection {
totalCount: Int!
cursor: String
items: [Branch]
}
type CommitCollection {
totalCount: Int!
cursor: String
items: [Commit]
}
type CommitCollectionUser {
totalCount: Int!
cursor: String
items: [CommitCollectionUserNode]
}
extend type Mutation {
branchCreate(branch: BranchCreateInput!): String!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
branchUpdate(branch: BranchUpdateInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
branchDelete(branch: BranchDeleteInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
commitCreate(commit: CommitCreateInput!): String!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
commitUpdate(commit: CommitUpdateInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
commitDelete(commit: CommitDeleteInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
}
extend type Subscription {
# TODO: auth for these subscriptions
"""
Subscribe to branch created event
"""
branchCreated(streamId: String!): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to branch updated event.
"""
branchUpdated(streamId: String!, branchId: String): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to branch deleted event
"""
branchDeleted(streamId: String!): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to commit created event
"""
commitCreated(streamId: String!): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to commit updated event.
"""
commitUpdated(streamId: String!, commitId: String): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to commit deleted event
"""
commitDeleted(streamId: String!): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
}
input BranchCreateInput {
streamId: String!
name: String!
description: String
}
input BranchUpdateInput {
streamId: String!
id: String!
name: String
description: String
}
input BranchDeleteInput {
streamId: String!
id: String!
}
input CommitCreateInput {
streamId: String!
branchName: String!
objectId: String!
message: String
previousCommitIds: [String]
}
input CommitUpdateInput {
streamId: String!
id: String!
message: String!
}
input CommitDeleteInput {
streamId: String!
id: String!
}
,extend type Stream {
object( id: String! ): Object
}
type Object {
id: String!
speckleType: String!
applicationId: String
createdAt: DateTime
totalChildrenCount: Int
"""
The full object, with all its props & other things. **NOTE:** If you're requesting objects for the purpose of recreating & displaying, you probably only want to request this specific field.
"""
data: JSONObject
"""
Get any objects that this object references. In the case of commits, this will give you a commit's constituent objects.
**NOTE**: Providing any of the two last arguments ( `query`, `orderBy` ) will trigger a different code branch that executes a much more expensive SQL query. It is not recommended to do so for basic clients that are interested in purely getting all the objects of a given commit.
"""
children(
limit: Int! = 100,
depth: Int! = 50,
select: [String],
cursor: String,
query: [JSONObject!],
orderBy: JSONObject ): ObjectCollection!
}
type ObjectCollection {
totalCount: Int!
cursor: String
objects: [Object]!
}
extend type Mutation {
objectCreate( objectInput: ObjectCreateInput! ): [String]!
}
input ObjectCreateInput {
"""
The stream against which these objects will be created.
"""
streamId: String!
"""
The objects you want to create.
"""
objects: [JSONObject]!
},extend type Query {
serverInfo: ServerInfo!
}
"""
Information about this server.
"""
type ServerInfo {
name: String!
company: String
description: String
adminContact: String
canonicalUrl: String
termsOfService: String
roles: [Role]!
scopes: [Scope]!
}
"""
Available roles.
"""
type Role {
name: String!
description: String!
resourceTarget: String!
}
"""
Available scopes.
"""
type Scope {
name: String!
description: String!
}
extend type Mutation {
serverInfoUpdate(info: ServerInfoUpdateInput!): Boolean
@hasRole(role: "server:admin")
@hasScope(scope: "server:setup")
}
input ServerInfoUpdateInput {
name: String!
company: String
description: String
adminContact: String
termsOfService: String
}
,extend type Query {
"""
Returns a specific stream.
"""
stream( id: String! ): Stream
"""
All the streams of the current user, pass in the `query` parameter to search by name, description or ID.
"""
streams( query: String, limit: Int = 25, cursor: String ): StreamCollection
@hasScope(scope: "streams:read")
}
type Stream {
id: String!
name: String!
description: String
isPublic: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
collaborators: [ StreamCollaborator ]!
}
extend type User {
"""
All the streams that a user has access to.
"""
streams( limit: Int! = 25, cursor: String ): StreamCollection
}
type StreamCollaborator {
id: String!
name: String!
role: String!
company: String
avatar: String
}
type StreamCollection {
totalCount: Int!
cursor: String
items: [ Stream ]
}
extend type Mutation {
"""
Creates a new stream.
"""
streamCreate( stream: StreamCreateInput! ): String
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Updates an existing stream.
"""
streamUpdate( stream: StreamUpdateInput! ): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Deletes an existing stream.
"""
streamDelete( id: String! ): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Grants permissions to a user on a given stream.
"""
streamGrantPermission( permissionParams: StreamGrantPermissionInput! ): Boolean
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Revokes the permissions of a user on a given stream.
"""
streamRevokePermission( permissionParams: StreamRevokePermissionInput! ): Boolean
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
}
extend type Subscription {
#
# User bound subscriptions that operate on the stream collection of an user
# Example relevant view/usecase: updating the list of streams for a user.
#
"""
Subscribes to new stream added event for your profile. Use this to display an up-to-date list of streams.
**NOTE**: If someone shares a stream with you, this subscription will be triggered with an extra value of `sharedBy` in the payload.
"""
userStreamAdded: JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "profile:read")
"""
Subscribes to stream removed event for your profile. Use this to display an up-to-date list of streams for your profile.
**NOTE**: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of `revokedBy` in the payload.
"""
userStreamRemoved: JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "profile:read")
#
# Stream bound subscriptions that operate on the stream itself.
# Example relevant view/usecase: a single stream connector, or view, or component in a web app
#
"""
Subscribes to stream updated event. Use this in clients/components that pertain only to this stream.
"""
streamUpdated( streamId: String ): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribes to stream deleted event. Use this in clients/components that pertain only to this stream.
"""
streamDeleted( streamId: String ): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
}
input StreamCreateInput {
name: String
description: String
isPublic: Boolean
}
input StreamUpdateInput {
id: String!
name: String
description: String
isPublic: Boolean
}
input StreamGrantPermissionInput {
streamId: String!,
userId: String!,
role: String!
}
input StreamRevokePermissionInput {
streamId: String!,
userId: String!
}
,extend type Query {
"""
Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
"""
user(id: String): User
userSearch(
query: String!
limit: Int! = 25
cursor: String
): UserSearchResultCollection
userPwdStrength(pwd: String!): JSONObject
}
"""
Base user type.
"""
type User {
id: String!
suuid: String
email: String
name: String
bio: String
company: String
avatar: String
verified: Boolean
profiles: JSONObject
role: String
}
type UserSearchResultCollection {
cursor: String
items: [UserSearchResult]
}
type UserSearchResult {
id: String!
name: String
bio: String
company: String
avatar: String
verified: Boolean
}
extend type Mutation {
"""
Edits a user's profile.
"""
userUpdate(user: UserUpdateInput!): Boolean!
}
input UserUpdateInput {
name: String
company: String
bio: String
}
-35
View File
@@ -1,35 +0,0 @@
from typing import Any, List
class SpeckleException(Exception):
def __init__(self, message: str, exception: Exception = None) -> None:
self.message = message
self.exception = exception
def __str__(self) -> str:
return f"SpeckleException: {self.message}"
class SerializationException(SpeckleException):
def __init__(self, message: str, object: Any, exception: Exception = None) -> None:
super().__init__(message=message)
self.object = object
self.unhandled_type = type(object)
def __str__(self) -> str:
return f"SpeckleException: Could not serialize object of type {self.unhandled_type}"
class GraphQLException(SpeckleException):
def __init__(self, message: str, errors: List, data=None) -> None:
super().__init__(message=message)
self.errors = errors
self.data = data
def __str__(self) -> str:
return f"GraphQLException: {self.message}"
class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None:
super().__init__(*args)
-5
View File
@@ -1,5 +0,0 @@
"""Builtin Speckle object kit."""
from specklepy.objects.base import Base
__all__ = ["Base"]
-772
View File
@@ -1,772 +0,0 @@
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
GEOMETRY = "Objects.Geometry."
class Interval(Base, speckle_type="Objects.Primitive.Interval"):
start: float = 0.0
end: float = 0.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
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":
"""Create a new Point from a list of three floats representing the x, y, and z coordinates"""
return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[Any]:
return [self.x, self.y, self.z]
@classmethod
def from_coords(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0):
"""Create a new Point from x, y, and z values"""
pt = Point()
pt.x, pt.y, pt.z = x, y, z
return pt
class Vector(Point, speckle_type=GEOMETRY + "Vector"):
pass
class ControlPoint(Point, speckle_type=GEOMETRY + "ControlPoint"):
weight: float = None
class Plane(Base, speckle_type=GEOMETRY + "Plane"):
origin: Point = Point()
normal: Vector = Vector()
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()
ySize: Interval = Interval()
zSize: Interval = Interval()
xSize: Interval = Interval()
area: float = None
volume: float = None
class Line(Base, speckle_type=GEOMETRY + "Line"):
start: Point = Point()
end: Point = None
domain: Interval = None
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
startAngle: float = None
endAngle: float = None
angleRadians: float = None
plane: Plane = None
domain: Interval = None
startPoint: Point = None
midPoint: Point = None
endPoint: Point = None
bbox: Box = None
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
plane: Plane = None
domain: Interval = None
bbox: Box = None
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
secondRadius: float = None
plane: Plane = None
domain: Interval = None
trimDomain: Interval = None
bbox: Box = None
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}):
value: List[float] = None
closed: bool = None
domain: Interval = None
bbox: Box = None
area: float = None
length: float = None
@classmethod
def from_points(cls, points: List[Point]):
"""Create a new Polyline from a list of Points"""
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]),
)
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
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.value:
return
if len(self.value) % 3:
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
]
class Curve(
Base,
speckle_type=GEOMETRY + "Curve",
chunkable={"points": 20000, "weights": 20000, "knots": 20000},
):
degree: int = None
periodic: bool = None
rational: bool = None
points: List[float] = None
weights: List[float] = None
knots: List[float] = None
domain: Interval = None
displayValue: Polyline = None
closed: bool = None
bbox: Box = None
area: float = None
length: float = None
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.points:
return
if len(self.points) % 3:
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 = int(args[7])
weights_count = int(args[8])
knots_count = int(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=int(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
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: List[Base] = None
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
profile: Base = None
pathStart: Point = None
pathEnd: Point = None
pathCurve: Base = None
pathTangent: Base = None
profiles: List[Base] = None
length: float = None
area: float = None
volume: float = None
bbox: Box = None
class Mesh(
Base,
speckle_type=GEOMETRY + "Mesh",
chunkable={
"vertices": 2000,
"faces": 2000,
"colors": 2000,
"textureCoordinates": 2000,
},
):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
textureCoordinates: List[float] = None
bbox: Box = None
area: float = None
volume: float = None
@classmethod
def create(
cls,
vertices: List[float],
faces: List[int],
colors: List[int] = None,
texture_coordinates: List[float] = None,
) -> "Mesh":
"""
Create a new Mesh from lists representing its vertices, faces,
colors (optional), and texture coordinates (optional).
This will initialise empty lists for colors and texture coordinates
if you do not provide any.
"""
return cls(
vertices=vertices,
faces=faces,
colors=colors or [],
textureCoordinates=texture_coordinates or [],
)
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: int = None
degreeV: int = None
rational: bool = None
area: float = None
pointData: List[float] = None
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"):
_Brep: "Brep" = None
SurfaceIndex: int = None
LoopIndices: List[int] = None
OuterLoopIndex: int = None
OrientationReversed: bool = None
@property
def _outer_loop(self):
return self._Brep.Loops[self.OuterLoopIndex]
@property
def _surface(self):
return self._Brep.Surfaces[self.SurfaceIndex]
@property
def _loops(self):
if self.LoopIndices:
return [self._Brep.Loops[i] for i in self.LoopIndices]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
_Brep: "Brep" = None
Curve3dIndex: int = None
TrimIndices: List[int] = None
StartIndex: int = None
EndIndex: int = None
ProxyCurveIsReversed: bool = None
Domain: Interval = None
@property
def _start_vertex(self):
return self._Brep.Vertices[self.StartIndex]
@property
def _end_vertex(self):
return self._Brep.Vertices[self.EndIndex]
@property
def _trims(self):
if self.TrimIndices:
return [self._Brep.Trims[i] for i in self.TrimIndices]
@property
def _curve(self):
return self._Brep.Curve3D[self.Curve3dIndex]
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
_Brep: "Brep" = None
FaceIndex: int = None
TrimIndices: List[int] = None
Type: str = None
@property
def _face(self):
return self._Brep.Faces[self.FaceIndex]
@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
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
_Brep: "Brep" = None
EdgeIndex: int = None
StartIndex: int = None
EndIndex: int = None
FaceIndex: int = None
LoopIndex: int = None
CurveIndex: int = None
IsoStatus: int = None
TrimType: str = None
IsReversed: bool = None
Domain: Interval = None
@property
def _face(self):
return self._Brep.Faces[self.FaceIndex]
@property
def _loop(self):
return self._Brep.Loops[self.LoopIndex]
@property
def _edge(self):
return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
@property
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"},
):
provenance: str = None
bbox: Box = None
area: float = None
volume: float = None
_displayValue: List[Mesh] = None
Surfaces: List[Surface] = None
Curve3D: List[Base] = None
Curve2D: List[Base] = None
Vertices: List[Point] = None
IsClosed: bool = None
Orientation: int = None
def _inject_self_into_children(self, children: Optional[List[Base]]) -> List[Base]:
if children is None:
return children
for child in children:
child._Brep = self
return children
# set as prop for now for backwards compatibility
@property
def displayValue(self) -> List[Mesh]:
return self._displayValue
@displayValue.setter
def displayValue(self, value):
if isinstance(value, Mesh):
self._displayValue = [value]
elif isinstance(value, list):
self._displayValue = value
@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)
]
BrepEdge.update_forward_refs()
BrepLoop.update_forward_refs()
BrepTrim.update_forward_refs()
BrepFace.update_forward_refs()
-40
View File
@@ -1,40 +0,0 @@
"""Builtin Speckle object kit."""
from specklepy.objects.structural.analysis import *
from specklepy.objects.structural.properties import *
from specklepy.objects.structural.material import *
from specklepy.objects.structural.geometry import *
from specklepy.objects.structural.loading import *
from specklepy.objects.structural.axis import Axis
__all__ = [
"Element1D",
"Element2D",
"Element3D",
"Axis",
"Node",
"Restraint",
"Load",
"LoadBeam",
"LoadCase",
"LoadCombinations",
"LoadFace",
"LoadGravity",
"LoadNode",
"Model",
"ModelInfo",
"ModelSettings",
"ModelUnits",
"Concrete",
"Material",
"Steel",
"Timber",
"Property",
"Property1D",
"Property2D",
"Property3D",
"PropertyDamper",
"PropertyMass",
"PropertySpring",
"SectionProfile",
]
-51
View File
@@ -1,51 +0,0 @@
from typing import List
from ..base import Base
from ..geometry import *
from .properties import *
STRUCTURAL_ANALYSIS = "Objects.Structural.Analysis."
class ModelUnits(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelUnits"):
length: str = None
sections: str = None
displacements: str = None
stress: str = None
force: str = None
mass: str = None
time: str = None
temperature: str = None
velocity: str = None
acceleration: str = None
energy: str = None
angle: str = None
strain: str = None
class ModelSettings(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelSettings"):
modelUnits: ModelUnits = None
steelCode: str = None
concreteCode: str = None
coincidenceTolerance: float = 0.0
class ModelInfo(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelInfo"):
name: str = None
description: str = None
projectNumber: str = None
projectName: str = None
settings: ModelSettings = None
initials: str = None
application: str = None
class Model(Base, speckle_type=STRUCTURAL_ANALYSIS + "Model"):
specs: ModelInfo = None
nodes: List = None
elements: List = None
loads: List = None
restraints: List = None
properties: List = None
materials: List = None
layerDescription: str = None
-8
View File
@@ -1,8 +0,0 @@
from ..base import Base
from ..geometry import Plane
class Axis(Base, speckle_type="Objects.Structural.Geometry.Axis"):
name: str = None
axisType: str = None
plane: Plane = None
-108
View File
@@ -1,108 +0,0 @@
from enum import Enum
from typing import List
from ..base import Base
from ..geometry import *
from .properties import *
from .axis import Axis
STRUCTURAL_GEOMETRY = "Objects.Structural.Geometry"
class ElementType1D(int, Enum):
Beam = 0
Brace = 1
Bar = 2
Column = 3
Rod = 4
Spring = 5
Tie = 6
Strut = 7
Link = 8
Damper = 9
Cable = 10
Spacer = 11
Other = 12
Null = 13
class ElementType2D(int, Enum):
Quad4 = 0
Quad8 = 1
Triangle3 = 2
Triangle6 = 3
class ElementType3D(int, Enum):
Brick8 = 0
Wedge6 = 1
Pyramid5 = 2
Tetra4 = 3
class Restraint(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Restraint"):
code: str = None
stiffnessX: float = 0.0
stiffnessY: float = 0.0
stiffnessZ: float = 0.0
stiffnessXX: float = 0.0
stiffnessYY: float = 0.0
stiffnessZZ: float = 0.0
units: str = None
class Node(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Node"):
name: str = None
basePoint: Point = None
constraintAxis: Axis = None
restraint: Restraint = None
springProperty: PropertySpring = None
massProperty: PropertyMass = None
damperProperty: PropertyDamper = None
units: str = None
class Element1D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element1D"):
name: str = None
baseLine: Line = None
property: Property1D = None
type: ElementType1D = None
end1Releases: Restraint = None
end2Releases: Restraint = None
end1Offset: Vector = None
end2Offset: Vector = None
orientationNode: Node = None
orinetationAngle: float = 0.0
localAxis: Plane = None
parent: Base = None
end1Node: Node = Node
end2Node: Node = Node
topology: List = None
displayMesh: Mesh = None
units: str = None
class Element2D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element2D"):
name: str = None
property: Property2D = None
type: ElementType2D = None
offset: float = 0.0
orientationAngle: float = 0.0
parent: Base = None
topology: List = None
displayMesh: Mesh = None
units: str = None
class Element3D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element3D"):
name: str = None
baseMesh: Mesh = None
property: Property3D = None
type: ElementType3D = None
orientationAngle: float = 0.0
parent: Base = None
topology: List
units: str = None
# class Storey needs ependency on built elements first
-174
View File
@@ -1,174 +0,0 @@
from typing import List
from ..base import Base
from ..geometry import *
from .loading import *
from .geometry import *
from .analysis import Model
STRUCTURAL_RESULTS = "Objects.Structural.Results."
class Result(Base, speckle_type=STRUCTURAL_RESULTS + "Result"):
resultCase: Base = None
permutation: str = None
description: str = None
class ResultSet1D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet1D"):
results1D: List
class Result1D(Result, speckle_type=STRUCTURAL_RESULTS + "Result1D"):
element: Element1D = None
position: float = 0.0
dispX: float = 0.0
dispY: float = 0.0
dispZ: float = 0.0
rotXX: float = 0.0
rotYY: float = 0.0
rotZZ: float = 0.0
forceX: float = 0.0
forceY: float = 0.0
forceZ: float = 0.0
momentXX: float = 0.0
momentYY: float = 0.0
momentZZ: float = 0.0
axialStress: float = 0.0
shearStressY: float = 0.0
shearStressZ: float = 0.0
bendingStressYPos: float = 0.0
bendingStressYNeg: float = 0.0
bendingStressZPos: float = 0.0
bendingStressZNeg: float = 0.0
combinedStressMax: float = 0.0
combinedStressMin: float = 0.0
class ResultSet2D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet2D"):
results2D: List
class Result2D(Result, speckle_type=STRUCTURAL_RESULTS + "Result2D"):
element: Element2D = None
position: List
dispX: float = 0.0
dispY: float = 0.0
dispZ: float = 0.0
forceXX: float = 0.0
forceYY: float = 0.0
forceXY: float = 0.0
momentXX: float = 0.0
momentYY: float = 0.0
momentXY: float = 0.0
shearX: float = 0.0
shearY: float = 0.0
stressTopXX: float = 0.0
stressTopYY: float = 0.0
stressTopZZ: float = 0.0
stressTopXY: float = 0.0
stressTopYZ: float = 0.0
stressTopZX: float = 0.0
stressMidXX: float = 0.0
stressMidYY: float = 0.0
stressMidZZ: float = 0.0
stressMidXY: float = 0.0
stressMidYZ: float = 0.0
stressMidZX: float = 0.0
stressBotXX: float = 0.0
stressBotYY: float = 0.0
stressBotZZ: float = 0.0
stressBotXY: float = 0.0
stressBotYZ: float = 0.0
stressBotZX: float = 0.0
class ResultSet3D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet3D"):
results3D: List
class Result3D(Result, speckle_type=STRUCTURAL_RESULTS + "Result3D"):
element: Element3D = None
position: List
dispX: float = 0.0
dispY: float = 0.0
dispZ: float = 0.0
stressXX: float = 0.0
stressYY: float = 0.0
stressZZ: float = 0.0
stressXY: float = 0.0
stressYZ: float = 0.0
stressZX: float = 0.0
class ResultGlobal(Result, speckle_type=STRUCTURAL_RESULTS + "ResultGlobal"):
model: Model = None
loadX: float = 0.0
loadY: float = 0.0
loadZ: float = 0.0
loadXX: float = 0.0
loadYY: float = 0.0
loadZZ: float = 0.0
reactionX: float = 0.0
reactionY: float = 0.0
reactionZ: float = 0.0
reactionXX: float = 0.0
reactionYY: float = 0.0
reactionZZ: float = 0.0
mode: float = 0.0
frequency: float = 0.0
loadFactor: float = 0.0
modalStiffness: float = 0.0
modalGeoStiffness: float = 0.0
effMassX: float = 0.0
effMassY: float = 0.0
effMassZ: float = 0.0
effMassXX: float = 0.0
effMassYY: float = 0.0
effMassZZ: float = 0.0
class ResultSetNode(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSetNode"):
resultsNode: List
class ResultNode(Result, speckle_type=STRUCTURAL_RESULTS + " ResultNode"):
node: Node = None
dispX: float = 0.0
dispY: float = 0.0
dispZ: float = 0.0
rotXX: float = 0.0
rotYY: float = 0.0
rotZZ: float = 0.0
reactionX: float = 0.0
reactionY: float = 0.0
reactionZ: float = 0.0
reactionXX: float = 0.0
reactionYY: float = 0.0
reactionZZ: float = 0.0
constraintX: float = 0.0
constraintY: float = 0.0
constraintZ: float = 0.0
constraintXX: float = 0.0
constraintYY: float = 0.0
constraintZZ: float = 0.0
velX: float = 0.0
velY: float = 0.0
velZ: float = 0.0
velXX: float = 0.0
velYY: float = 0.0
velZZ: float = 0.0
accX: float = 0.0
accY: float = 0.0
accZ: float = 0.0
accXX: float = 0.0
accYY: float = 0.0
accZZ: float = 0.0
class ResultSetAll(Base, speckle_type=None):
resultSet1D: ResultSet1D = None
resultSet2D: ResultSet2D = None
resultSet3D: ResultSet3D = None
resultsGlobal: ResultGlobal = None
resultsNode: ResultSetNode = None
-64
View File
@@ -1,64 +0,0 @@
from warnings import warn
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
UNITS = ["mm", "cm", "m", "in", "ft", "yd", "mi"]
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"],
"mi": ["mi", "mile", "miles"],
"none": ["none", "null"],
}
UNITS_ENCODINGS = {
"none": 0,
"mm": 1,
"cm": 2,
"m": 3,
"km": 4,
"in": 5,
"ft": 6,
"yd": 7,
"mi": 8,
}
def get_units_from_string(unit: str):
if not isinstance(unit, str):
warn(
f"Invalid units: expected type str but received {type(unit)} ({unit}). Skipping - no units will be set.",
SpeckleWarning,
)
return
unit = str.lower(unit)
for name, alternates in UNITS_STRINGS.items():
if unit in alternates:
return name
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
View File
@@ -1 +0,0 @@
from .server import ServerTransport
@@ -1,36 +1,38 @@
import re
from warnings import warn
from deprecated import deprecated
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.logging import metrics
from specklepy.logging.exceptions import (
SpeckleException,
SpeckleWarning,
)
from typing import Dict
from warnings import warn
from specklepy.api import resources
from specklepy.api.resources import (
branch,
commit,
stream,
object,
server,
user,
subscriptions,
)
from specklepy.api.models import ServerInfo
from deprecated import deprecated
from gql import Client
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.websockets import WebsocketsTransport
from specklepy.api import resources
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.api.resources import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
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`.
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)
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
and added your account.
```py
@@ -93,15 +95,22 @@ class SpeckleClient:
# ) from ex
def __repr__(self):
return f"SpeckleClient( server: {self.url}, authenticated: {self.account.token is not None} )"
return (
f"SpeckleClient( server: {self.url}, authenticated:"
f" {self.account.token is not None} )"
)
@deprecated(
version="2.6.0",
reason="Renamed: please use `authenticate_with_account` or `authenticate_with_token` instead.",
reason=(
"Renamed: please use `authenticate_with_account` or"
" `authenticate_with_token` instead."
),
)
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
The token is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments:
token {str} -- an api token
@@ -110,8 +119,10 @@ class SpeckleClient:
self._set_up_client()
def authenticate_with_token(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
"""
Authenticate the client using a personal access token.
The token is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments:
token {str} -- an api token
@@ -122,10 +133,12 @@ class SpeckleClient:
def authenticate_with_account(self, account: Account) -> None:
"""Authenticate the client using an Account object
The account is saved in the client object and a synchronous GraphQL entrypoint is created
The account is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments:
account {Account} -- the account object which can be found with `get_default_account` or `get_local_accounts`
account {Account} -- the account object which can be found with
`get_default_account` or `get_local_accounts`
"""
metrics.track(metrics.CLIENT, account, {"name": "authenticate with account"})
self.account = account
@@ -136,6 +149,8 @@ class SpeckleClient:
headers = {
"Authorization": f"Bearer {self.account.token}",
"Content-Type": "application/json",
"apollographql-client-name": metrics.HOST_APP,
"apollographql-client-version": metrics.HOST_APP_VERSION,
}
httptransport = RequestsHTTPTransport(
url=self.graphql, headers=headers, verify=True, retries=3
@@ -152,7 +167,8 @@ class SpeckleClient:
if self.user.get() is None:
warn(
SpeckleWarning(
f"Possibly invalid token - could not authenticate Speckle Client for server {self.url}"
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
)
)
@@ -160,9 +176,38 @@ class SpeckleClient:
return self.httpclient.execute(query)
def _init_resources(self) -> None:
self.stream = stream.Resource(
self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
try:
server_version = self.server.version()
except Exception:
pass
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.other_user = other_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = active_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.commit = commit.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
@@ -172,12 +217,6 @@ class SpeckleClient:
self.object = object.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.user = user.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.subscribe = subscriptions.Resource(
account=self.account,
basepath=self.ws_url,
@@ -190,7 +229,7 @@ class SpeckleClient:
return attr.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
except:
except AttributeError:
raise SpeckleException(
f"Method {name} is not supported by the SpeckleClient class"
)
@@ -1,29 +1,35 @@
import os
from pydantic import BaseModel, Field
from typing import List, Optional
from specklepy.logging import metrics
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from specklepy.api.models import ServerInfo
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
class UserInfo(BaseModel):
name: Optional[str]
email: Optional[str]
company: Optional[str]
id: Optional[str]
name: Optional[str] = None
email: Optional[str] = None
company: Optional[str] = None
id: Optional[str] = None
class Account(BaseModel):
isDefault: bool = False
token: str = None
refreshToken: str = None
token: Optional[str] = None
refreshToken: Optional[str] = None
serverInfo: ServerInfo = Field(default_factory=ServerInfo)
userInfo: UserInfo = Field(default_factory=UserInfo)
id: str = None
id: Optional[str] = None
def __repr__(self) -> str:
return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})"
return (
f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url},"
f" isDefault: {self.isDefault})"
)
def __str__(self) -> str:
return self.__repr__()
@@ -35,24 +41,40 @@ class Account(BaseModel):
return acct
def get_local_accounts(base_path: str = None) -> List[Account]:
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
"""Gets all the accounts present in this environment
Arguments:
base_path {str} -- custom base path if you are not using the system default
Returns:
List[Account] -- list of all local accounts or an empty list if no accounts were found
List[Account] -- list of all local accounts or an empty list if
no accounts were found
"""
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)
json_acct_files = [file for file in os.listdir(json_path) if file.endswith(".json")]
accounts: List[Account] = []
try:
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
res = account_storage.get_all_objects()
account_storage.close()
if res:
accounts.extend(Account.parse_raw(r[1]) for r in res)
except SpeckleException:
# cannot open SQLiteTransport, probably because of the lack
# of disk write permissions
pass
json_acct_files = []
json_path = str(speckle_path_provider.accounts_folder_path())
try:
os.makedirs(json_path, exist_ok=True)
json_acct_files.extend(
file for file in os.listdir(json_path) if file.endswith(".json")
)
except Exception:
# cannot find or get the json account paths
pass
accounts = []
res = account_storage.get_all_objects()
if res:
accounts.extend(Account.parse_raw(r[1]) for r in res)
if json_acct_files:
try:
accounts.extend(
@@ -63,7 +85,8 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
raise SpeckleException(
"Invalid json accounts could not be read. Please fix or remove them.",
ex,
)
) from ex
metrics.track(
metrics.ACCOUNTS,
next(
@@ -75,8 +98,10 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
return accounts
def get_default_account(base_path: str = None) -> Account:
"""Gets this environment's default account if any. If there is no default, the first found will be returned and set as default.
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
"""
Gets this environment's default account if any. If there is no default,
the first found will be returned and set as default.
Arguments:
base_path {str} -- custom base path if you are not using the system default
@@ -102,7 +127,8 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
token {str} -- the api token
Returns:
Account -- the local account with this token or a shell account containing just the token and url if no local account is found
Account -- the local account with this token or a shell account containing
just the token and url if no local account is found
"""
accounts = get_local_accounts()
if not accounts:
@@ -126,6 +152,9 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
class StreamWrapper:
def __init__(self, url: str = None) -> None:
raise SpeckleException(
message="The StreamWrapper has moved as of v2.6.0! Please import from specklepy.api.wrapper",
exception=DeprecationWarning,
message=(
"The StreamWrapper has moved as of v2.6.0! Please import from"
" specklepy.api.wrapper"
),
exception=DeprecationWarning(),
)
+116
View File
@@ -0,0 +1,116 @@
from dataclasses import dataclass
from enum import Enum
from unicodedata import name
class HostAppVersion(Enum):
v = "v"
v6 = "v6"
v7 = "v7"
v2019 = "v2019"
v2020 = "v2020"
v2021 = "v2021"
v2022 = "v2022"
v2023 = "v2023"
v2024 = "v2024"
v2025 = "v2025"
vSandbox = "vSandbox"
vRevit = "vRevit"
vRevit2021 = "vRevit2021"
vRevit2022 = "vRevit2022"
vRevit2023 = "vRevit2023"
vRevit2024 = "vRevit2024"
vRevit2025 = "vRevit2025"
v25 = "v25"
v26 = "v26"
def __repr__(self) -> str:
return self.value
def __str__(self) -> str:
return self.value
@dataclass
class HostApplication:
name: str
slug: str
def get_version(self, version: HostAppVersion) -> str:
return f"{name.replace(' ', '')}{str(version).strip('v')}"
RHINO = HostApplication("Rhino", "rhino")
GRASSHOPPER = HostApplication("Grasshopper", "grasshopper")
REVIT = HostApplication("Revit", "revit")
DYNAMO = HostApplication("Dynamo", "dynamo")
UNITY = HostApplication("Unity", "unity")
GSA = HostApplication("GSA", "gsa")
CIVIL = HostApplication("Civil 3D", "civil3d")
AUTOCAD = HostApplication("AutoCAD", "autocad")
MICROSTATION = HostApplication("MicroStation", "microstation")
OPENROADS = HostApplication("OpenRoads", "openroads")
OPENRAIL = HostApplication("OpenRail", "openrail")
OPENBUILDINGS = HostApplication("OpenBuildings", "openbuildings")
ETABS = HostApplication("ETABS", "etabs")
SAP2000 = HostApplication("SAP2000", "sap2000")
CSIBRIDGE = HostApplication("CSIBridge", "csibridge")
SAFE = HostApplication("SAFE", "safe")
TEKLASTRUCTURES = HostApplication("Tekla Structures", "teklastructures")
DXF = HostApplication("DXF Converter", "dxf")
EXCEL = HostApplication("Excel", "excel")
UNREAL = HostApplication("Unreal", "unreal")
POWERBI = HostApplication("Power BI", "powerbi")
BLENDER = HostApplication("Blender", "blender")
QGIS = HostApplication("QGIS", "qgis")
ARCGIS = HostApplication("ArcGIS", "arcgis")
SKETCHUP = HostApplication("SketchUp", "sketchup")
ARCHICAD = HostApplication("Archicad", "archicad")
TOPSOLID = HostApplication("TopSolid", "topsolid")
PYTHON = HostApplication("Python", "python")
NET = HostApplication(".NET", "net")
OTHER = HostApplication("Other", "other")
_app_name_host_app_mapping = {
"dynamo": DYNAMO,
"revit": REVIT,
"autocad": AUTOCAD,
"civil": CIVIL,
"rhino": RHINO,
"grasshopper": GRASSHOPPER,
"unity": UNITY,
"gsa": GSA,
"microstation": MICROSTATION,
"openroads": OPENROADS,
"openrail": OPENRAIL,
"openbuildings": OPENBUILDINGS,
"etabs": ETABS,
"sap": SAP2000,
"csibridge": CSIBRIDGE,
"safe": SAFE,
"teklastructures": TEKLASTRUCTURES,
"dxf": DXF,
"excel": EXCEL,
"unreal": UNREAL,
"powerbi": POWERBI,
"blender": BLENDER,
"qgis": QGIS,
"arcgis": ARCGIS,
"sketchup": SKETCHUP,
"archicad": ARCHICAD,
"topsolid": TOPSOLID,
"python": PYTHON,
"net": NET,
}
def get_host_app_from_string(app_name: str) -> HostApplication:
app_name = app_name.lower().replace(" ", "")
for partial_app_name, host_app in _app_name_host_app_mapping.items():
if partial_app_name in app_name:
return host_app
return HostApplication(app_name, app_name)
if __name__ == "__main__":
print(HostAppVersion.v)
+198
View File
@@ -0,0 +1,198 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
class Collaborator(BaseModel):
id: Optional[str]
name: Optional[str]
role: Optional[str]
avatar: Optional[str]
class Commit(BaseModel):
id: Optional[str]
message: Optional[str]
authorName: Optional[str]
authorId: Optional[str]
authorAvatar: Optional[str]
branchName: Optional[str]
createdAt: Optional[datetime]
sourceApplication: Optional[str]
referencedObject: Optional[str]
totalChildrenCount: Optional[int]
parents: Optional[List[str]]
def __repr__(self) -> str:
return (
f"Commit( id: {self.id}, message: {self.message}, referencedObject:"
f" {self.referencedObject}, authorName: {self.authorName}, branchName:"
f" {self.branchName}, createdAt: {self.createdAt} )"
)
def __str__(self) -> str:
return self.__repr__()
class Commits(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Commit] = []
class Object(BaseModel):
id: Optional[str]
speckleType: Optional[str]
applicationId: Optional[str]
totalChildrenCount: Optional[int]
createdAt: Optional[datetime]
class Branch(BaseModel):
id: Optional[str]
name: Optional[str]
description: Optional[str]
commits: Optional[Commits]
class Branches(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Branch] = []
class Stream(BaseModel):
id: Optional[str] = None
name: Optional[str]
role: Optional[str] = None
isPublic: Optional[bool] = None
description: Optional[str] = None
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
collaborators: List[Collaborator] = Field(default_factory=list)
branches: Optional[Branches] = None
commit: Optional[Commit] = None
object: Optional[Object] = None
commentCount: Optional[int] = None
favoritedDate: Optional[datetime] = None
favoritesCount: Optional[int] = None
def __repr__(self):
return (
f"Stream( id: {self.id}, name: {self.name}, description:"
f" {self.description}, isPublic: {self.isPublic})"
)
def __str__(self) -> str:
return self.__repr__()
class Streams(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Stream] = []
class User(BaseModel):
id: Optional[str]
email: Optional[str]
name: Optional[str]
bio: Optional[str]
company: Optional[str]
avatar: Optional[str]
verified: Optional[bool]
role: Optional[str]
streams: Optional[Streams]
def __repr__(self):
return (
f"User( id: {self.id}, name: {self.name}, email: {self.email}, company:"
f" {self.company} )"
)
def __str__(self) -> str:
return self.__repr__()
class LimitedUser(BaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
name: Optional[str]
bio: Optional[str]
company: Optional[str]
avatar: Optional[str]
verified: Optional[bool]
role: Optional[str]
class PendingStreamCollaborator(BaseModel):
id: Optional[str]
inviteId: Optional[str]
streamId: Optional[str]
streamName: Optional[str]
title: Optional[str]
role: Optional[str]
invitedBy: Optional[User]
user: Optional[User]
token: Optional[str]
def __repr__(self):
return (
f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId:"
f" {self.streamId}, role: {self.role}, title: {self.title}, invitedBy:"
f" {self.user.name if self.user else None})"
)
def __str__(self) -> str:
return self.__repr__()
class Activity(BaseModel):
actionType: Optional[str]
info: Optional[dict]
userId: Optional[str]
streamId: Optional[str]
resourceId: Optional[str]
resourceType: Optional[str]
message: Optional[str]
time: Optional[datetime]
def __repr__(self) -> str:
return (
f"Activity( streamId: {self.streamId}, actionType: {self.actionType},"
f" message: {self.message}, userId: {self.userId} )"
)
def __str__(self) -> str:
return self.__repr__()
class ActivityCollection(BaseModel):
totalCount: Optional[int]
items: Optional[List[Activity]]
cursor: Optional[datetime]
def __repr__(self) -> str:
return (
f"ActivityCollection( totalCount: {self.totalCount}, items:"
f" {len(self.items) if self.items else 0}, cursor:"
f" {self.cursor.isoformat() if self.cursor else None} )"
)
def __str__(self) -> str:
return self.__repr__()
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
url: Optional[str] = None
description: Optional[str] = None
adminContact: Optional[str] = None
canonicalUrl: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
@@ -1,16 +1,16 @@
from typing import List
from typing import List, Optional
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.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.transports.sqlite import SQLiteTransport
def send(
base: Base,
transports: List[AbstractTransport] = None,
transports: Optional[List[AbstractTransport]] = None,
use_default_cache: bool = True,
):
"""Sends an object via the provided transports. Defaults to the local cache.
@@ -18,7 +18,8 @@ def send(
Arguments:
obj {Base} -- the object you want to send
transports {list} -- where you want to send them
use_default_cache {bool} -- toggle for the default cache. If set to false, it will only send to the provided transports
use_default_cache {bool} -- toggle for the default cache.
If set to false, it will only send to the provided transports
Returns:
str -- the object id of the sent object
@@ -26,9 +27,15 @@ def send(
if not transports and not use_default_cache:
raise SpeckleException(
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
message=(
"You need to provide at least one transport: cannot send with an empty"
" transport list and no default cache"
)
)
if isinstance(transports, AbstractTransport):
transports = [transports]
if transports is None:
metrics.track(metrics.SEND)
transports = []
@@ -47,8 +54,17 @@ def send(
def receive(
obj_id: str,
remote_transport: AbstractTransport = None,
local_transport: AbstractTransport = None,
remote_transport: Optional[AbstractTransport] = None,
local_transport: Optional[AbstractTransport] = None,
) -> Base:
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
return _untracked_receive(obj_id, remote_transport, local_transport)
def _untracked_receive(
obj_id: str,
remote_transport: Optional[AbstractTransport] = None,
local_transport: Optional[AbstractTransport] = None,
) -> Base:
"""Receives an object from a transport.
@@ -61,20 +77,22 @@ def receive(
Returns:
Base -- the base object
"""
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
if not local_transport:
local_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=local_transport)
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialisation using the local transport
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport
obj_string = local_transport.get_object(obj_id)
if obj_string:
return serializer.read_json(obj_string=obj_string)
if not remote_transport:
raise SpeckleException(
message="Could not find the specified object using the local transport, and you didn't provide a fallback remote from which to pull it."
message=(
"Could not find the specified object using the local transport, and you"
" didn't provide a fallback remote from which to pull it."
)
)
obj_string = remote_transport.copy_object_and_children(
@@ -86,12 +104,14 @@ def receive(
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
"""
Serialize a base object. If no write transports are provided, the object will be serialized
Serialize a base object. If no write transports are provided,
the object will be serialized
without detaching or chunking any of the attributes.
Arguments:
base {Base} -- the object to serialize
write_transports {List[AbstractTransport]} -- optional: the transports to write to
write_transports {List[AbstractTransport]}
-- optional: the transports to write to
Returns:
str -- the serialized object
@@ -102,14 +122,21 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
return serializer.write_json(base)[1]
def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Base:
def deserialize(
obj_string: str, read_transport: Optional[AbstractTransport] = None
) -> 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.
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.
Arguments:
obj_string {str} -- the string object to deserialize
read_transport {AbstractTransport} -- the transport to fetch children objects from
(defaults to SQLiteTransport)
read_transport {AbstractTransport}
-- the transport to fetch children objects from
(defaults to SQLiteTransport)
Returns:
Base -- the deserialized object
@@ -121,3 +148,6 @@ def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Ba
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
__all__ = ["receive", "send", "serialize", "deserialize"]
+128
View File
@@ -0,0 +1,128 @@
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from gql.client import Client
from gql.transport.exceptions import TransportQueryError
from graphql import DocumentNode
from specklepy.api.credentials import Account
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
UnsupportedException,
)
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
class ResourceBase(object):
def __init__(
self,
account: Account,
basepath: str,
client: Client,
name: str,
server_version: Optional[Tuple[Any, ...]] = None,
) -> None:
self.account = account
self.basepath = basepath
self.client = client
self.name = name
self.server_version = server_version
self.schema: Optional[Type] = None
def _step_into_response(self, response: dict, return_type: Union[str, List, None]):
"""Step into the dict to get the relevant data"""
if return_type is None:
return response
if isinstance(return_type, str):
return response[return_type]
if isinstance(return_type, List):
for key in return_type:
response = response[key]
return response
def _parse_response(self, response: Union[dict, list, None], schema=None):
"""Try to create a class instance from the response"""
if response is None:
return None
if isinstance(response, list):
return [self._parse_response(response=r, schema=schema) for r in response]
if schema:
return schema.parse_obj(response)
elif self.schema:
try:
return self.schema.parse_obj(response)
except Exception:
s = BaseObjectSerializer(read_transport=SQLiteTransport())
return s.recompose_base(response)
else:
return response
def make_request(
self,
query: DocumentNode,
params: Optional[Dict] = None,
return_type: Union[str, List, None] = None,
schema=None,
parse_response: bool = True,
) -> Any:
"""Executes the GraphQL query"""
try:
response = self.client.execute(query, variable_values=params)
except Exception as ex:
if isinstance(ex, TransportQueryError):
return GraphQLException(
message=(
f"Failed to execute the GraphQL {self.name} request. Errors:"
f" {ex.errors}"
),
errors=ex.errors,
data=ex.data,
)
else:
return SpeckleException(
message=(
f"Failed to execute the GraphQL {self.name} request. Inner"
f" exception: {ex}"
),
exception=ex,
)
response = self._step_into_response(response=response, return_type=return_type)
if parse_response:
return self._parse_response(response=response, schema=schema)
else:
return response
def _check_server_version_at_least(
self, target_version: Tuple[Any, ...], unsupported_message: Optional[str] = None
):
"""Use this check to guard against making unsupported requests on older servers.
Arguments:
target_version {tuple}
the minimum server version in the format (major, minor, patch, (tag, build))
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
"""
if not unsupported_message:
unsupported_message = (
"The client method used is not supported on Speckle Server versions"
f" prior to v{'.'.join(target_version)}"
)
# if version is dev, it should be supported... (or not)
if self.server_version == ("dev",):
return
if self.server_version and self.server_version < target_version:
raise UnsupportedException(unsupported_message)
def _check_invites_supported(self):
"""Invites are only supported for Speckle Server >= 2.6.4.
Use this check to guard against making unsupported requests on older servers.
"""
self._check_server_version_at_least(
(2, 6, 4),
"Stream invites are only supported as of Speckle Server v2.6.4. Please"
" update your Speckle Server to use this method or use the"
" `grant_permission` flow instead.",
)
@@ -1,12 +1,8 @@
from pathlib import Path
import sys
import inspect
import pkgutil
import sys
from importlib import import_module
for (_, name, _) in pkgutil.iter_modules(__path__):
for _, name, _ in pkgutil.iter_modules(__path__):
imported_module = import_module("." + name, package=__name__)
if hasattr(imported_module, "Resource"):
+269
View File
@@ -0,0 +1,269 @@
from datetime import datetime, timezone
from typing import List, Optional
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
NAME = "active_user"
class Resource(ResourceBase):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = User
def get(self) -> User:
"""Gets the profile of a user. If no id argument is provided,
will return the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
metrics.track(metrics.USER, self.account, {"name": "get"})
query = gql(
"""
query User {
activeUser {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
"""
)
params = {}
return self.make_request(query=query, params=params, return_type="activeUser")
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.USER, self.account, {"name": "update"})
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
userUpdate(user: $user)
}
"""
)
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
params = {"user": {k: v for k, v in params.items() if v is not None}}
if not params["user"]:
return SpeckleException(
message=(
"You must provide at least one field to update your user profile"
)
)
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's
activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
activeUser {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["activeUser", "activity"],
schema=ActivityCollection,
)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvites {
streamInvites{
id
token
inviteId
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
return self.make_request(
query=query,
return_type="streamInvites",
schema=PendingStreamCollaborator,
)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvite($streamId: String!, $token: String) {
streamInvite(streamId: $streamId, token: $token) {
id
token
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
params = {"streamId": stream_id}
if token:
params["token"] = token
return self.make_request(
query=query,
params=params,
return_type="streamInvite",
schema=PendingStreamCollaborator,
)
@@ -1,10 +1,12 @@
from typing import Optional
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Branch
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
NAME = "branch"
METHODS = ["create"]
class Resource(ResourceBase):
@@ -16,7 +18,6 @@ class Resource(ResourceBase):
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
self.schema = Branch
@@ -88,7 +89,7 @@ class Resource(ResourceBase):
createdAt
}
}
}
}
}
}
"""
@@ -114,7 +115,11 @@ class Resource(ResourceBase):
metrics.track(metrics.BRANCH, self.account, {"name": "get"})
query = gql(
"""
query BranchesGet($stream_id: String!, $branches_limit: Int!, $commits_limit: Int!) {
query BranchesGet(
$stream_id: String!,
$branches_limit: Int!,
$commits_limit: Int!
) {
stream(id: $stream_id) {
branches(limit: $branches_limit) {
items {
@@ -153,7 +158,11 @@ class Resource(ResourceBase):
)
def update(
self, stream_id: str, branch_id: str, name: str = None, description: str = None
self,
stream_id: str,
branch_id: str,
name: Optional[str] = None,
description: Optional[str] = None,
):
"""Update a branch
@@ -1,12 +1,12 @@
from typing import Optional, List
from typing import List, Optional
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Commit
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
NAME = "commit"
METHODS = []
class Resource(ResourceBase):
@@ -18,7 +18,6 @@ class Resource(ResourceBase):
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
self.schema = Commit
@@ -117,9 +116,13 @@ class Resource(ResourceBase):
Arguments:
stream_id {str} -- the stream you want to commit to
object_id {str} -- the hash of your commit object
branch_name {str} -- the name of the branch to commit to (defaults to "main")
message {str} -- optional: a message to give more information about the commit
source_application{str} -- optional: the application from which the commit was created (defaults to "python")
branch_name {str}
-- the name of the branch to commit to (defaults to "main")
message {str}
-- optional: a message to give more information about the commit
source_application{str}
-- optional: the application from which the commit was created
(defaults to "python")
parents {List[str]} -- optional: the id of the parent commits
Returns:
@@ -128,7 +131,8 @@ class Resource(ResourceBase):
metrics.track(metrics.COMMIT, self.account, {"name": "create"})
query = gql(
"""
mutation CommitCreate ($commit: CommitCreateInput!){ commitCreate(commit: $commit)}
mutation CommitCreate ($commit: CommitCreateInput!)
{ commitCreate(commit: $commit)}
"""
)
params = {
@@ -152,7 +156,8 @@ class Resource(ResourceBase):
Update a commit
Arguments:
stream_id {str} -- the id of the stream that contains the commit you'd like to update
stream_id {str}
-- the id of the stream that contains the commit you'd like to update
commit_id {str} -- the id of the commit you'd like to update
message {str} -- the updated commit message
@@ -162,7 +167,8 @@ class Resource(ResourceBase):
metrics.track(metrics.COMMIT, self.account, {"name": "update"})
query = gql(
"""
mutation CommitUpdate($commit: CommitUpdateInput!){ commitUpdate(commit: $commit)}
mutation CommitUpdate($commit: CommitUpdateInput!)
{ commitUpdate(commit: $commit)}
"""
)
params = {
@@ -178,7 +184,8 @@ class Resource(ResourceBase):
Delete a commit
Arguments:
stream_id {str} -- the id of the stream that contains the commit you'd like to delete
stream_id {str}
-- the id of the stream that contains the commit you'd like to delete
commit_id {str} -- the id of the commit you'd like to delete
Returns:
@@ -187,7 +194,8 @@ class Resource(ResourceBase):
metrics.track(metrics.COMMIT, self.account, {"name": "delete"})
query = gql(
"""
mutation CommitDelete($commit: CommitDeleteInput!){ commitDelete(commit: $commit)}
mutation CommitDelete($commit: CommitDeleteInput!)
{ commitDelete(commit: $commit)}
"""
)
params = {"commit": {"streamId": stream_id, "id": commit_id}}
@@ -1,10 +1,11 @@
from typing import Dict, List
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.objects.base import Base
NAME = "object"
METHODS = []
class Resource(ResourceBase):
@@ -16,7 +17,6 @@ class Resource(ResourceBase):
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
self.schema = Base
@@ -61,21 +61,28 @@ class Resource(ResourceBase):
"""
Not advised - generally, you want to use `operations.send()`.
Create a new object on a stream. To send a base object, you can prepare it by running it through the
`BaseObjectSerializer.traverse_base()` function to get a valid (serialisable) object to send.
Create a new object on a stream.
To send a base object, you can prepare it by running it through the
`BaseObjectSerializer.traverse_base()` function to get a valid (serialisable)
object to send.
NOTE: this does not create a commit - you can create one with `SpeckleClient.commit.create`. Dynamic fields will be located in the 'data' dict of the received `Base` object
NOTE: this does not create a commit - you can create one with
`SpeckleClient.commit.create`.
Dynamic fields will be located in the 'data' dict of the received `Base` object
Arguments:
stream_id {str} -- the id of the stream you want to send the object to
objects {List[Dict]} -- a list of base dictionary objects (NOTE: must be json serialisable)
objects {List[Dict]}
-- a list of base dictionary objects (NOTE: must be json serialisable)
Returns:
str -- the id of the object
"""
query = gql(
"""
mutation ObjectCreate($object_input: ObjectCreateInput!) { objectCreate(objectInput: $object_input) }
mutation ObjectCreate($object_input: ObjectCreateInput!) {
objectCreate(objectInput: $object_input)
}
"""
)
params = {"object_input": {"streamId": stream_id, "objects": objects}}
@@ -1,50 +1,50 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from gql import gql
from specklepy.api.models import ActivityCollection, LimitedUser
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from typing import List
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.api.models import ActivityCollection, User
NAME = "user"
METHODS = ["get", "search", "update", "activity"]
NAME = "other_user"
class Resource(ResourceBase):
"""API Access class for users"""
"""API Access class for other users, that are not the currently active user."""
def __init__(self, account, basepath, client) -> None:
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
server_version=server_version,
)
self.schema = User
self.schema = LimitedUser
def get(self, id: str = None) -> User:
"""Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
LimitedUser -- the retrieved profile of another user
"""
metrics.track(metrics.USER, self.account, {"name": "get"})
metrics.track(metrics.OTHER_USER, self.account, {"name": "get"})
query = gql(
"""
query User($id: String) {
user(id: $id) {
query OtherUser($id: String!) {
otherUser(id: $id) {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
@@ -53,23 +53,26 @@ class Resource(ResourceBase):
params = {"id": id}
return self.make_request(query=query, params=params, return_type="user")
return self.make_request(query=query, params=params, return_type="otherUser")
def search(self, search_query: str, limit: int = 25) -> List[User]:
"""Searches for user by name or email. The search query must be at least 3 characters long
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""Searches for user by name or email. The search query must be at least
3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[User] -- a list of User objects that match the search query
List[LimitedUser] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
)
metrics.track(metrics.USER, self.account, {"name": "search"})
metrics.track(metrics.OTHER_USER, self.account, {"name": "search"})
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
@@ -92,69 +95,51 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["userSearch", "items"]
)
def update(
self, name: str = None, company: str = None, bio: str = None, avatar: str = None
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns:
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.USER, self.account, {"name": "update"})
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
userUpdate(user: $user)
}
"""
)
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
params = {"user": {k: v for k, v in params.items() if v is not None}}
if not params["user"]:
return SpeckleException(
message="You must provide at least one field to update your user profile"
)
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
def activity(
self,
user_id: str = None,
user_id: str,
limit: int = 20,
action_type: str = None,
before: datetime = None,
after: datetime = None,
cursor: datetime = None,
):
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's activity (as extracted from the authorization header).
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity($user_id: String, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
user(id: $user_id) {
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
query UserActivity(
$user_id: String!,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
otherUser(id: $user_id) {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
@@ -185,6 +170,6 @@ class Resource(ResourceBase):
return self.make_request(
query=query,
params=params,
return_type=["user", "activity"],
return_type=["otherUser", "activity"],
schema=ActivityCollection,
)
@@ -1,12 +1,14 @@
from typing import Dict, List
import re
from typing import Any, Dict, List, Tuple
from gql import gql
from specklepy.api.models import ServerInfo
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import GraphQLException
NAME = "server"
METHODS = ["get", "apps"]
class Resource(ResourceBase):
@@ -18,7 +20,6 @@ class Resource(ResourceBase):
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
def get(self) -> ServerInfo:
@@ -61,6 +62,39 @@ class Resource(ResourceBase):
query=query, return_type="serverInfo", schema=ServerInfo
)
def version(self) -> Tuple[Any, ...]:
"""Get the server version
Returns:
the server version in the format (major, minor, patch, (tag, build))
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
"""
# not tracking as it will be called along with other mutations / queries as a check
query = gql(
"""
query Server {
serverInfo {
version
}
}
"""
)
ver = self.make_request(
query=query, return_type=["serverInfo", "version"], parse_response=False
)
if isinstance(ver, Exception):
raise GraphQLException(
f"Could not get server version for {self.basepath}", [ver]
)
# pylint: disable=consider-using-generator; (list comp is faster)
return tuple(
[
int(segment) if segment.isdigit() else segment
for segment in re.split(r"\.|-", ver)
]
)
def apps(self) -> Dict:
"""Get the apps registered on the server
@@ -1,26 +1,27 @@
from datetime import datetime, timezone
from gql import gql
from typing import List
from specklepy.logging import metrics
from specklepy.api.models import ActivityCollection, Stream
from specklepy.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, Stream
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
NAME = "stream"
METHODS = ["list", "create", "get", "update", "delete", "search", "activity"]
class Resource(ResourceBase):
"""API Access class for streams"""
def __init__(self, account, basepath, client) -> None:
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
server_version=server_version,
)
self.schema = Stream
@@ -152,7 +153,8 @@ class Resource(ResourceBase):
Arguments:
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool} -- whether or not the stream can be viewed by anyone with the id
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
id {str} -- the id of the newly created stream
@@ -175,7 +177,11 @@ class Resource(ResourceBase):
)
def update(
self, id: str, name: str = None, description: str = None, is_public: bool = None
self,
id: str,
name: Optional[str] = None,
description: Optional[str] = None,
is_public: Optional[bool] = None,
) -> bool:
"""Update an existing stream
@@ -183,7 +189,8 @@ class Resource(ResourceBase):
id {str} -- the id of the stream to be updated
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool} -- whether or not the stream can be viewed by anyone with the id
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
bool -- whether the stream update was successful
@@ -255,7 +262,12 @@ class Resource(ResourceBase):
metrics.track(metrics.STREAM, self.account, {"name": "search"})
query = gql(
"""
query StreamSearch($search_query: String!,$limit: Int!, $branch_limit:Int!, $commit_limit:Int!) {
query StreamSearch(
$search_query: String!,
$limit: Int!,
$branch_limit:Int!,
$commit_limit:Int!
) {
streams(query: $search_query, limit: $limit) {
items {
id
@@ -314,7 +326,8 @@ class Resource(ResourceBase):
Arguments:
stream_id {str} -- the id of the stream to favorite / unfavorite
favorited {bool} -- whether to favorite (True) or unfavorite (False) the stream
favorited {bool}
-- whether to favorite (True) or unfavorite (False) the stream
Returns:
Stream -- the stream with its `id`, `name`, and `favoritedDate`
@@ -342,9 +355,18 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["streamFavorite"]
)
@deprecated(
version="2.6.4",
reason=(
"As of Speckle Server v2.6.4, this method is deprecated. Users need to be"
" invited and accept the invite before being added to a stream"
),
)
def grant_permission(self, stream_id: str, user_id: str, role: str):
"""Grant permissions to a user on a given stream
Valid for Speckle Server version < 2.6.4
Arguments:
stream_id {str} -- the id of the stream to grant permissions to
user_id {str} -- the id of the user to grant permissions for
@@ -354,9 +376,23 @@ class Resource(ResourceBase):
bool -- True if the operation was successful
"""
metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role})
# we're checking for the actual version info, and if the version is 'dev' we treat it
# as an up to date instance
if self.server_version and (
self.server_version == ("dev",) or self.server_version >= (2, 6, 4)
):
raise UnsupportedException(
"Server mutation `grant_permission` is no longer supported as of"
" Speckle Server v2.6.4. Please use the new `update_permission` method"
" to change an existing user's permission or use the `invite` method to"
" invite a user to a stream."
)
query = gql(
"""
mutation StreamGrantPermission($permission_params: StreamGrantPermissionInput !) {
mutation StreamGrantPermission(
$permission_params: StreamGrantPermissionInput !
) {
streamGrantPermission(permissionParams: $permission_params)
}
"""
@@ -377,6 +413,298 @@ class Resource(ResourceBase):
parse_response=False,
)
def get_all_pending_invites(
self, stream_id: str
) -> List[PendingStreamCollaborator]:
"""Get all of the pending invites on a stream.
You must be a `stream:owner` to query this.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the stream id from which to get the pending invites
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the specified stream
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvites($streamId: String!) {
stream(id: $streamId){
pendingCollaborators {
id
token
inviteId
streamId
streamName
title
role
invitedBy{
id
name
company
avatar
}
user {
id
name
company
avatar
}
}
}
}
"""
)
params = {"streamId": stream_id}
return self.make_request(
query=query,
params=params,
return_type=["stream", "pendingCollaborators"],
schema=PendingStreamCollaborator,
)
def invite(
self,
stream_id: str,
email: Optional[str] = None,
user_id: Optional[str] = None,
role: str = "stream:contributor", # should default be reviewer?
message: Optional[str] = None,
):
"""Invite someone to a stream using either their email or user id
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
email {str} -- the email of the user to invite (use this OR `user_id`)
user_id {str} -- the id of the user to invite (use this OR `email`)
role {str}
-- the role to assign to the user (defaults to `stream:contributor`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.INVITE, self.account, {"name": "create"})
self._check_invites_supported()
if email is None and user_id is None:
raise SpeckleException(
"You must provide either an email or a user id to use the"
" `stream.invite` method"
)
query = gql(
"""
mutation StreamInviteCreate($input: StreamInviteCreateInput!) {
streamInviteCreate(input: $input)
}
"""
)
params = {
"email": email,
"userId": user_id,
"streamId": stream_id,
"message": message,
"role": role,
}
params = {"input": {k: v for k, v in params.items() if v is not None}}
return self.make_request(
query=query,
params=params,
return_type="streamInviteCreate",
parse_response=False,
)
def invite_batch(
self,
stream_id: str,
emails: Optional[List[str]] = None,
user_ids: Optional[List[None]] = None,
message: Optional[str] = None,
) -> bool:
"""Invite a batch of users to a specified stream.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
emails {List[str]}
-- the email of the user to invite (use this and/or `user_ids`)
user_id {List[str]}
-- the id of the user to invite (use this and/or `emails`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.INVITE, self.account, {"name": "batch create"})
self._check_invites_supported()
if emails is None and user_ids is None:
raise SpeckleException(
"You must provide either an email or a user id to use the"
" `stream.invite` method"
)
query = gql(
"""
mutation StreamInviteBatchCreate($input: [StreamInviteCreateInput!]!) {
streamInviteBatchCreate(input: $input)
}
"""
)
email_invites = [
{"streamId": stream_id, "message": message, "email": email}
for email in emails
if emails is not None
]
user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id}
for user_id in user_ids
if user_ids is not None
]
params = {"input": [*email_invites, *user_invites]}
return self.make_request(
query=query,
params=params,
return_type="streamInviteBatchCreate",
parse_response=False,
)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream invite
invite_id {str} -- the id of the invite to use
Returns:
bool -- true if the operation was successful
"""
metrics.track(metrics.INVITE, self.account, {"name": "cancel"})
self._check_invites_supported()
query = gql(
"""
mutation StreamInviteCancel($streamId: String!, $inviteId: String!) {
streamInviteCancel(streamId: $streamId, inviteId: $inviteId)
}
"""
)
params = {"streamId": stream_id, "inviteId": invite_id}
return self.make_request(
query=query,
params=params,
return_type="streamInviteCancel",
parse_response=False,
)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str}
-- the id of the stream for which the user has a pending invite
token {str} -- the token of the invite to use
accept {bool} -- whether or not to accept the invite (defaults to True)
Returns:
bool -- true if the operation was successful
"""
metrics.track(metrics.INVITE, self.account, {"name": "use"})
self._check_invites_supported()
query = gql(
"""
mutation StreamInviteUse(
$accept: Boolean!,
$streamId: String!,
$token: String!
) {
streamInviteUse(accept: $accept, streamId: $streamId, token: $token)
}
"""
)
params = {"streamId": stream_id, "token": token, "accept": accept}
return self.make_request(
query=query,
params=params,
return_type="streamInviteUse",
parse_response=False,
)
def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream
Valid for Speckle Server >=2.6.4
Arguments:
stream_id {str} -- the id of the stream to grant permissions to
user_id {str} -- the id of the user to grant permissions for
role {str} -- the role to grant the user
Returns:
bool -- True if the operation was successful
"""
metrics.track(
metrics.PERMISSION, self.account, {"name": "update", "role": role}
)
if self.server_version and (
self.server_version != ("dev",) and self.server_version < (2, 6, 4)
):
raise UnsupportedException(
"Server mutation `update_permission` is only supported as of Speckle"
" Server v2.6.4. Please update your Speckle Server to use this method"
" or use the `grant_permission` method instead."
)
query = gql(
"""
mutation StreamUpdatePermission(
$permission_params: StreamUpdatePermissionInput!
) {
streamUpdatePermission(permissionParams: $permission_params)
}
"""
)
params = {
"permission_params": {
"streamId": stream_id,
"userId": user_id,
"role": role,
}
}
return self.make_request(
query=query,
params=params,
return_type="streamUpdatePermission",
parse_response=False,
)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
@@ -390,7 +718,9 @@ class Resource(ResourceBase):
metrics.track(metrics.PERMISSION, self.account, {"name": "revoke"})
query = gql(
"""
mutation StreamRevokePermission($permission_params: StreamRevokePermissionInput !) {
mutation StreamRevokePermission(
$permission_params: StreamRevokePermissionInput!
) {
streamRevokePermission(permissionParams: $permission_params)
}
"""
@@ -408,29 +738,48 @@ class Resource(ResourceBase):
def activity(
self,
stream_id: str,
action_type: str = None,
action_type: Optional[str] = None,
limit: int = 20,
before: datetime = None,
after: datetime = None,
cursor: datetime = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
Note: all timestamps arguments should be `datetime` of any tz
as they will be converted to UTC ISO format strings
stream_id {str} -- the id of the stream to get activity from
action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
action_type {str}
-- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
before {datetime}
-- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query StreamActivity($stream_id: String!, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
query StreamActivity(
$stream_id: String!,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
stream(id: $stream_id) {
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
@@ -463,8 +812,9 @@ class Resource(ResourceBase):
}
except AttributeError as e:
raise SpeckleException(
"Could not get stream activity - `before`, `after`, and `cursor` must be in `datetime` format if provided",
ValueError,
"Could not get stream activity - `before`, `after`, and `cursor` must"
" be in `datetime` format if provided",
ValueError(),
) from e
return self.make_request(
@@ -1,16 +1,14 @@
from typing import Callable, Dict, List
from functools import wraps
from typing import Callable, Dict, List, Optional, Union
from gql import gql
from graphql import DocumentNode
from specklepy.api.resource import ResourceBase
from specklepy.api.resources.stream import Stream
from specklepy.logging.exceptions import SpeckleException
NAME = "subscribe"
METHODS = [
"stream_added",
"stream_updated",
"stream_removed",
]
def check_wsclient(function):
@@ -35,15 +33,16 @@ class Resource(ResourceBase):
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
@check_wsclient
async def stream_added(self, callback: Callable = None):
"""Subscribes to new stream added event for your profile. Use this to display an up-to-date list of streams.
async def stream_added(self, callback: Optional[Callable] = None):
"""Subscribes to new stream added event for your profile.
Use this to display an up-to-date list of streams.
Arguments:
callback {Callable[Stream]} -- a function that takes the updated stream as an argument and executes each time a stream is added
callback {Callable[Stream]} -- a function that takes the updated stream
as an argument and executes each time a stream is added
Returns:
Stream -- the update stream
@@ -58,12 +57,16 @@ class Resource(ResourceBase):
)
@check_wsclient
async def stream_updated(self, id: str, callback: Callable = None):
"""Subscribes to stream updated event. Use this in clients/components that pertain only to this stream.
async def stream_updated(self, id: str, callback: Optional[Callable] = None):
"""
Subscribes to stream updated event.
Use this in clients/components that pertain only to this stream.
Arguments:
id {str} -- the stream id of the stream to subscribe to
callback {Callable[Stream]} -- a function that takes the updated stream as an argument and executes each time the stream is updated
callback {Callable[Stream]}
-- a function that takes the updated stream
as an argument and executes each time the stream is updated
Returns:
Stream -- the update stream
@@ -84,11 +87,17 @@ class Resource(ResourceBase):
)
@check_wsclient
async def stream_removed(self, callback: Callable = None):
"""Subscribes to stream removed event for your profile. Use this to display an up-to-date list of streams for your profile. NOTE: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of revokedBy in the payload.
async def stream_removed(self, callback: Optional[Callable] = None):
"""Subscribes to stream removed event for your profile.
Use this to display an up-to-date list of streams for your profile.
NOTE: If someone revokes your permissions on a stream,
this subscription will be triggered with an extra value of revokedBy
in the payload.
Arguments:
callback {Callable[Dict]} -- a function that takes the returned dict as an argument and executes each time a stream is removed
callback {Callable[Dict]}
-- a function that takes the returned dict as an argument
and executes each time a stream is removed
Returns:
dict -- dict containing 'id' of stream removed and optionally 'revokedBy'
@@ -109,15 +118,15 @@ class Resource(ResourceBase):
@check_wsclient
async def subscribe(
self,
query: gql,
params: Dict = None,
callback: Callable = None,
return_type: str or List = None,
query: DocumentNode,
params: Optional[Dict] = None,
callback: Optional[Callable] = None,
return_type: Optional[Union[str, List]] = None,
schema=None,
parse_response: bool = True,
):
# if self.client.transport.websocket is None:
# TODO: add multiple subs to the same ws connection
# TODO: add multiple subs to the same ws connection
async with self.client as session:
async for res in session.subscribe(query, variable_values=params):
res = self._step_into_response(response=res, return_type=return_type)
+327
View File
@@ -0,0 +1,327 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
NAME = "user"
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = (
"The user resource is deprecated, please use the active_user or other_user"
" resources"
)
class Resource(ResourceBase):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = User
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get(self, id: Optional[str] = None) -> User:
"""
Gets the profile of a user.
If no id argument is provided, will return the current authenticated
user's profile (as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
metrics.track(metrics.USER, self.account, {"name": "get"})
query = gql(
"""
query User($id: String) {
user(id: $id) {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
"""
)
params = {"id": id}
return self.make_request(query=query, params=params, return_type="user")
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[User], SpeckleException]:
"""
Searches for user by name or email.
The search query must be at least 3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[User] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
)
metrics.track(metrics.USER, self.account, {"name": "search"})
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
}
}
}
"""
)
params = {"search_query": search_query, "limit": limit}
return self.make_request(
query=query, params=params, return_type=["userSearch", "items"]
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns:
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.USER, self.account, {"name": "update"})
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
userUpdate(user: $user)
}
"""
)
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
params = {"user": {k: v for k, v in params.items() if v is not None}}
if not params["user"]:
return SpeckleException(
message=(
"You must provide at least one field to update your user profile"
)
)
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def activity(
self,
user_id: Optional[str] = None,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated
user's activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as
they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime}
-- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$user_id: String,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
user(id: $user_id) {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"user_id": user_id,
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["user", "activity"],
schema=ActivityCollection,
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvites {
streamInvites{
id
token
inviteId
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
return self.make_request(
query=query,
return_type="streamInvites",
schema=PendingStreamCollaborator,
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvite($streamId: String!, $token: String) {
streamInvite(streamId: $streamId, token: $token) {
id
token
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
params = {"streamId": stream_id}
if token:
params["token"] = token
return self.make_request(
query=query,
params=params,
return_type="streamInvite",
schema=PendingStreamCollaborator,
)
@@ -1,23 +1,28 @@
from urllib.parse import unquote, urlparse
from warnings import warn
from urllib.parse import urlparse, unquote
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
)
from specklepy.logging import metrics
from specklepy.api.client import SpeckleClient
from specklepy.transports.server.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
class StreamWrapper:
"""
The `StreamWrapper` gives you some handy helpers to deal with urls and get authenticated clients and transports.
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
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
@@ -45,7 +50,10 @@ class StreamWrapper:
_account: Account = None
def __repr__(self):
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
return (
f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type:"
f" {self.type} )"
)
def __str__(self) -> str:
return self.__repr__()
@@ -71,7 +79,8 @@ class StreamWrapper:
if not segments or len(segments) < 2:
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."
)
while segments:
@@ -90,7 +99,8 @@ class StreamWrapper:
self.commit_id = segments.pop(0)
else:
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."
)
if not self.stream_id:
@@ -104,13 +114,18 @@ class StreamWrapper:
def get_account(self, token: str = None) -> Account:
"""
Gets an account object for this server from the local accounts db (added via Speckle Manager or a json file)
Gets an account object for this server from the local accounts db
(added via Speckle Manager or a json file)
"""
if self._account and self._account.token:
return self._account
self._account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
(
a
for a in get_local_accounts()
if self.host == urlparse(a.serverInfo.url).netloc
),
None,
)
@@ -124,14 +139,18 @@ class StreamWrapper:
def get_client(self, token: str = None) -> SpeckleClient:
"""
Gets an authenticated client for this server. You may provide a token if there aren't any local accounts on this
machine. If no account is found and no token is provided, an unauthenticated client is returned.
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)
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
SpeckleClient
-- authenticated with a corresponding local account or the provided token
"""
if self._client and token is None:
return self._client
@@ -155,11 +174,14 @@ class StreamWrapper:
def get_transport(self, token: str = None) -> ServerTransport:
"""
Gets a server transport for this stream using an authenticated client. If there is no local account for this
server and the client was not authenticated with a token, this will throw an exception.
Gets a server transport for this stream using an authenticated client.
If there is no local account for this
server and the client was not authenticated with a token,
this will throw an exception.
Returns:
ServerTransport -- constructed for this stream with a pre-authenticated client
ServerTransport -- constructed for this stream
with a pre-authenticated client
"""
if not self._account or not self._account.token:
self.get_account(token)
+6
View File
@@ -0,0 +1,6 @@
"""
This is the Core SDK module of `specklepy`.
This module should be kept in sync with the functionalities of our other SDKs especially
C# Core https://github.com/specklesystems/speckle-sharp/tree/main/Core/Core
"""
+1
View File
@@ -0,0 +1 @@
"""Common helpers module for Core."""
@@ -0,0 +1,115 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
from typing import Optional
from specklepy.logging.exceptions import SpeckleException
_user_data_env_var = "SPECKLE_USERDATA_PATH"
def _path() -> Optional[Path]:
"""Read the user data path override setting."""
path_override = os.environ.get(_user_data_env_var)
if path_override:
return Path(path_override)
return None
_application_name = "Speckle"
def override_application_name(application_name: str) -> None:
"""Override the global Speckle application name."""
global _application_name
_application_name = application_name
def override_application_data_path(path: Optional[str]) -> None:
"""
Override the global Speckle application data path.
If the value of path is `None` the environment variable gets deleted.
"""
if path:
os.environ[_user_data_env_var] = path
else:
os.environ.pop(_user_data_env_var, None)
_blob_folder_name = "Blobs"
def override_blob_storage_folder(blob_folder_name: str) -> None:
"""Override the global Blob storage folder name."""
global _blob_folder_name
_blob_folder_name = blob_folder_name
_accounts_folder_name = "Accounts"
def override_accounts_folder_name(accounts_folder_name: str) -> None:
"""Override the global Accounts folder name."""
global _accounts_folder_name
_accounts_folder_name = accounts_folder_name
_objects_folder_name = "Objects"
def override_objects_folder_name(objects_folder_name: str) -> None:
"""Override global Objects folder name."""
global _objects_folder_name
_objects_folder_name = objects_folder_name
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
path = base_path.joinpath(folder_name)
path.mkdir(exist_ok=True, parents=True)
return path
def user_application_data_path() -> Path:
"""Get the platform specific user configuration folder path"""
path_override = _path()
if path_override:
return path_override
try:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise SpeckleException(
message="Cannot get appdata path from environment."
)
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
# as that is used as an override
app_data_path = os.getenv("XDG_DATA_HOME")
if app_data_path:
return Path(app_data_path)
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise SpeckleException(
message="Failed to initialize user application data path.", exception=ex
)
def user_speckle_folder_path() -> Path:
"""Get the folder where the user's Speckle data should be stored."""
return _ensure_folder_exists(user_application_data_path(), _application_name)
def accounts_folder_path() -> Path:
"""Get the folder where the Speckle accounts data should be stored."""
return _ensure_folder_exists(user_speckle_folder_path(), _accounts_folder_name)
def blob_storage_path(path: Optional[Path] = None) -> Path:
return _ensure_folder_exists(path or user_speckle_folder_path(), _blob_folder_name)
+60
View File
@@ -0,0 +1,60 @@
from typing import Any, List, Optional
class SpeckleException(Exception):
def __init__(self, message: str, exception: Exception = None) -> None:
super().__init__()
self.message = message
self.exception = exception
def __str__(self) -> str:
return f"SpeckleException: {self.message}"
class SpeckleInvalidUnitException(SpeckleException):
def __init__(self, invalid_unit: Any) -> None:
super().__init__(
message=(
"Invalid units: expected type str but received"
f" {type(invalid_unit)} ({invalid_unit})."
),
exception=None,
)
class SerializationException(SpeckleException):
def __init__(self, message: str, obj: Any, exception: Exception = None) -> None:
super().__init__(message=message, exception=exception)
self.obj = obj
self.unhandled_type = type(obj)
def __str__(self) -> str:
return (
"SpeckleException: Could not serialize object of type"
f" {self.unhandled_type}"
)
class GraphQLException(SpeckleException):
def __init__(
self, message: str, errors: Optional[List[Any]] = None, data=None
) -> None:
super().__init__(message=message)
self.errors = errors
self.data = data
def __str__(self) -> str:
return f"GraphQLException: {self.message}"
class UnsupportedException(SpeckleException):
def __init__(self, message: str) -> None:
super().__init__(message=message)
def __str__(self) -> str:
return f"UnsupportedException: {self.message}"
class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None:
super().__init__(*args)
@@ -1,10 +1,14 @@
import socket
import sys
import queue
import contextlib
import getpass
import hashlib
import logging
import requests
import platform
import queue
import sys
import threading
from typing import Optional
import requests
"""
Anonymous telemetry to help us understand how to make a better Speckle.
@@ -12,7 +16,7 @@ This really helps us to deliver a better open source project and product!
"""
TRACK = True
HOST_APP = "python"
HOST_APP_VERSION = f"python {'.'.join(map(str, sys.version_info[:3]))}"
HOST_APP_VERSION = f"python {'.'.join(map(str, sys.version_info[:2]))}"
PLATFORMS = {"win32": "Windows", "cygwin": "Windows", "darwin": "Mac OS X"}
LOG = logging.getLogger(__name__)
@@ -23,9 +27,11 @@ RECEIVE = "Receive"
SEND = "Send"
STREAM = "Stream Action"
PERMISSION = "Permission Action"
INVITE = "Invite Action"
COMMIT = "Commit Action"
BRANCH = "Branch Action"
USER = "User Action"
OTHER_USER = "Other User Action"
SERVER = "Server Action"
CLIENT = "Speckle Client"
STREAM_WRAPPER = "Stream Wrapper"
@@ -46,13 +52,17 @@ def enable():
TRACK = True
def set_host_app(host_app: str, host_app_version: str = None):
def set_host_app(host_app: str, host_app_version: Optional[str] = None):
global HOST_APP, HOST_APP_VERSION
HOST_APP = host_app
HOST_APP_VERSION = host_app_version or HOST_APP_VERSION
def track(action: str, account: "Account" = None, custom_props: dict = None):
def track(
action: str,
account=None,
custom_props: Optional[dict] = None,
):
if not TRACK:
return
try:
@@ -75,10 +85,10 @@ def track(action: str, account: "Account" = None, custom_props: dict = None):
METRICS_TRACKER.queue.put_nowait(event_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))
LOG.debug(f"Error queueing metrics request: {str(ex)}")
def initialise_tracker(account: "Account" = None):
def initialise_tracker(account=None):
global METRICS_TRACKER
if not METRICS_TRACKER:
METRICS_TRACKER = MetricsTracker()
@@ -101,8 +111,7 @@ class Singleton(type):
class MetricsTracker(metaclass=Singleton):
analytics_url = "https://analytics.speckle.systems/track?ip=1"
analytics_token = "acd87c5a50b56df91a795e999812a3a4"
user_ip = None
last_user = None
last_user = ""
last_server = None
platform = None
sending_thread = None
@@ -114,12 +123,15 @@ class MetricsTracker(metaclass=Singleton):
)
self.platform = PLATFORMS.get(sys.platform, "linux")
self.sending_thread.start()
self.user_ip = socket.gethostbyname(socket.gethostname())
with contextlib.suppress(Exception):
node, user = platform.node(), getpass.getuser()
if node and user:
self.last_user = f"@{self.hash(f'{node}-{user}')}"
def set_last_user(self, email: str):
if not email:
return
self.last_user = "@" + self.hash(email)
self.last_user = f"@{self.hash(email)}"
def set_last_server(self, server: str):
if not server:
@@ -137,6 +149,6 @@ class MetricsTracker(metaclass=Singleton):
try:
session.post(self.analytics_url, json=event_params)
except Exception as ex:
LOG.error("Error sending metrics request: " + str(ex))
LOG.debug(f"Error sending metrics request: {str(ex)}")
self.queue.task_done()
+6
View File
@@ -0,0 +1,6 @@
"""Builtin Speckle object kit."""
from specklepy.objects import encoding, geometry, other, primitive, structural, units
from specklepy.objects.base import Base
__all__ = ["Base", "encoding", "geometry", "other", "units", "structural", "primitive"]
@@ -1,21 +1,24 @@
import contextlib
from enum import Enum
from inspect import isclass
from typing import (
Any,
ClassVar,
Dict,
List,
Optional,
Union,
Set,
Tuple,
Type,
Union,
get_type_hints,
)
import contextlib
from enum import EnumMeta
from warnings import warn
from stringcase import pascalcase
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.units import get_units_from_string
from specklepy.objects.units import Units, get_units_from_string
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
@@ -90,23 +93,71 @@ class _RegisteringBase:
"""
speckle_type: ClassVar[str]
_type_registry: ClassVar[Dict[str, "Base"]] = {}
_speckle_type_override: ClassVar[Optional[str]] = None
_speckle_namespace: ClassVar[Optional[str]] = None
_type_registry: ClassVar[Dict[str, Type["Base"]]] = {}
_attr_types: ClassVar[Dict[str, Type]] = {}
class Config:
validate_assignment = True
# dict of chunkable props and their max chunk size
_chunkable: Dict[str, int] = {}
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
_serialize_ignore: Set[str] = set()
@classmethod
def get_registered_type(cls, speckle_type: str) -> Optional[Type["Base"]]:
"""Get the registered type from the protected mapping via the `speckle_type`"""
return cls._type_registry.get(speckle_type, None)
for full_name in reversed(speckle_type.split(":")):
maybe_type = cls._type_registry.get(full_name, None)
if maybe_type:
return maybe_type
return None
@classmethod
def _determine_speckle_type(cls) -> str:
"""
This method brings the speckle_type construction in par with peckle-sharp/Core.
The implementation differs, because in Core the basis of the speckle_type if
type.FullName, which includes the dotnet namespace name too.
Copying that behavior is hard in python, where the concept of namespaces
means something entirely different.
So we enabled a speckle_type override mechanism, that enables
"""
base_name = "Base"
if cls.__name__ == base_name:
return base_name
bases = [
b._full_name()
for b in reversed(cls.mro())
if issubclass(b, Base) and b.__name__ != base_name
]
return ":".join(bases)
@classmethod
def _full_name(cls) -> str:
base_name = "Base"
if cls.__name__ == base_name:
return base_name
if cls._speckle_type_override:
return cls._speckle_type_override
# convert the module names to PascalCase to match c# namespace naming convention
# also drop specklepy from the beginning
namespace = ".".join(
pascalcase(m)
for m in filter(lambda name: name != "specklepy", cls.__module__.split("."))
)
return f"{namespace}.{cls.__name__}"
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,
chunkable: Optional[Dict[str, int]] = None,
detachable: Optional[Set[str]] = None,
serialize_ignore: Optional[Set[str]] = None,
**kwargs: Dict[str, Any],
):
"""
@@ -116,14 +167,15 @@ class _RegisteringBase:
initialization. This is reused to register each subclassing type into a class
level dictionary.
"""
if speckle_type in cls._type_registry:
cls._speckle_type_override = speckle_type
cls.speckle_type = cls._determine_speckle_type()
if cls._full_name() in cls._type_registry:
raise ValueError(
f"The speckle_type: {speckle_type} is already registered for type: "
f"{cls._type_registry[speckle_type].__name__}. "
f"Please choose a different type name."
f"{cls._type_registry[cls._full_name()].__name__}. "
"Please choose a different type name."
)
cls.speckle_type = speckle_type or cls.__name__
cls._type_registry[cls.speckle_type] = cls # type: ignore
cls._type_registry[cls._full_name()] = cls # type: ignore
try:
cls._attr_types = get_type_hints(cls)
except Exception:
@@ -138,16 +190,135 @@ class _RegisteringBase:
super().__init_subclass__(**kwargs)
# T = TypeVar("T")
# how i wish the code below would be correct, but we're also parsing into floats
# and converting into strings if the original type is string, but the value isn't
# def _validate_type(t: type, value: T) -> Tuple[bool, T]:
def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
# this should be reworked. Its only ok to return null for Optionals...
# if t is None and value is None:
if value is None:
return True, value
# after fixing the None t above, this should be
# if t is Any:
# if t is None:
if t is None or t is Any:
return True, value
if isclass(t) and issubclass(t, Enum):
if isinstance(value, t):
return True, value
if value in t._value2member_map_:
return True, t(value)
if getattr(t, "__module__", None) == "typing":
origin = getattr(t, "__origin__")
# below is what in nicer for >= py38
# origin = get_origin(t)
# recursive validation for Unions on both types preferring the fist type
if origin is Union:
# below is what in nicer for >= py38
# t_1, t_2 = get_args(t)
args = t.__args__ # type: ignore
for arg_t in args:
t_success, t_value = _validate_type(arg_t, value)
if t_success:
return True, t_value
return False, value
if origin is dict:
if not isinstance(value, dict):
return False, value
if value == {}:
return True, value
if not getattr(t, "__args__", None):
return True, value
t_key, t_value = t.__args__ # type: ignore
if (
getattr(t_key, "__name__", None),
getattr(t_value, "__name__", None),
) == ("KT", "VT"):
return True, value
# we're only checking the first item, but the for loop and return after
# evaluating the first item is the fastest way
for dict_key, dict_value in value.items():
valid_key, _ = _validate_type(t_key, dict_key)
valid_value, _ = _validate_type(t_value, dict_value)
if valid_key and valid_value:
return True, value
return False, value
if origin is list:
if not isinstance(value, list):
return False, value
if value == []:
return True, value
if not hasattr(t, "__args__"):
return True, value
t_items = t.__args__[0] # type: ignore
if getattr(t_items, "__name__", None) == "T":
return True, value
first_item_valid, _ = _validate_type(t_items, value[0])
if first_item_valid:
return True, value
return False, value
if origin is tuple:
if not isinstance(value, tuple):
return False, value
if not hasattr(t, "__args__"):
return True, value
args = t.__args__ # type: ignore
if args == tuple():
return True, value
# we're not checking for empty tuple, cause tuple lengths must match
if len(args) != len(value):
return False, value
values = []
for t_item, v_item in zip(args, value):
item_valid, item_value = _validate_type(t_item, v_item)
if not item_valid:
return False, value
values.append(item_value)
return True, tuple(values)
if origin is set:
if not isinstance(value, set):
return False, value
if not hasattr(t, "__args__"):
return True, value
t_items = t.__args__[0] # type: ignore
first_item_valid, _ = _validate_type(t_items, next(iter(value)))
if first_item_valid:
return True, value
return False, value
if isinstance(value, t):
return True, value
with contextlib.suppress(ValueError):
if t is float and value is not None:
return True, float(value)
# TODO: dafuq, i had to add this not list check
# but it would also fail for objects and other complex values
if t is str and value and not isinstance(value, list):
return True, str(value)
return False, value
class Base(_RegisteringBase):
id: Optional[str] = None
totalChildrenCount: Optional[int] = None
applicationId: Optional[str] = None
_units: str = "m"
# dict of chunkable props and their max chunk size
_chunkable: Dict[str, int] = {}
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
_serialize_ignore: Set[str] = set()
id: Union[str, None] = None
totalChildrenCount: Union[int, None] = None
applicationId: Union[str, None] = None
_units: Union[Units, None] = None
def __init__(self, **kwargs) -> None:
super().__init__()
@@ -215,11 +386,14 @@ class Base(_RegisteringBase):
@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
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
See `objects.geometry` for an example of how this is used with
the Brep class definitions.
"""
try:
cls._attr_types = get_type_hints(cls)
@@ -240,55 +414,27 @@ class Base(_RegisteringBase):
"Invalid Name: Base member names cannot contain characters '.' or '/'",
)
def _type_check(self, name: str, value: Any):
def _type_check(self, name: str, value: Any) -> 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],
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 or t is Any:
return value
valid, checked_value = _validate_type(t, value)
if value is None:
return None
if isinstance(t, EnumMeta) and (value in t._value2member_map_):
return t(value)
if t.__module__ == "typing":
origin = getattr(t, "__origin__")
t = (
tuple(getattr(sub_t, "__origin__", sub_t) for sub_t in t.__args__)
if origin is 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 isinstance(t, tuple):
t = t[0]
with contextlib.suppress(ValueError):
if t is float:
return float(value)
if t is str and value:
return str(value)
if valid:
return checked_value
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}':"
f"it expects type '{str(t)}',"
f"but received type '{type(value).__name__}'"
)
def add_chunkable_attrs(self, **kwargs: int) -> None:
@@ -296,7 +442,8 @@ class Base(_RegisteringBase):
Mark defined attributes as chunkable for serialisation
Arguments:
kwargs {int} -- the name of the attribute as the keyword and the chunk size as the arg
kwargs {int} -- the name of the attribute as the keyword
and the chunk size as the arg
"""
chunkable = {k: v for k, v in kwargs.items() if isinstance(v, int)}
self._chunkable = dict(self._chunkable, **chunkable)
@@ -306,19 +453,28 @@ class Base(_RegisteringBase):
Mark defined attributes as detachable for serialisation
Arguments:
names {Set[str]} -- the names of the attributes to detach as a set of strings
names {Set[str]} -- the names of the attributes to detach as a set of string
"""
self._detachable = self._detachable.union(names)
@property
def units(self):
return self._units
def units(self) -> Union[str, None]:
if self._units:
return self._units.value
return None
@units.setter
def units(self, value: str):
units = get_units_from_string(value)
if units:
self._units = units
def units(self, value: Union[str, Units, None]):
if value is None:
units = value
elif isinstance(value, Units):
units: Units = value
else:
units = get_units_from_string(value)
self._units = units
# except SpeckleInvalidUnitException as ex:
# warn(f"Units are reset to None. Reason {ex.message}")
# self._units = None
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
@@ -331,7 +487,7 @@ class Base(_RegisteringBase):
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 sorted(list(set(self.get_member_names()) - self._serialize_ignore))
def get_typed_member_names(self) -> List[str]:
"""Get all of the names of the defined (typed) properties of this object"""
@@ -348,13 +504,17 @@ class Base(_RegisteringBase):
def get_id(self, decompose: bool = False) -> str:
"""
Gets the id (a unique hash) of this object. This method fully serializes the object which,
in the case of large objects (with many sub-objects), has a tangible cost. Avoid using it!
Gets the id (a unique hash) of this object.
This method fully serializes the object which,
in the case of large objects (with many sub-objects), has a tangible cost.
Avoid using it!
Note: the hash of a decomposed object differs from that of a non-decomposed object
Note: the hash of a decomposed object differs from that of a
non-decomposed object
Arguments:
decompose {bool} -- if True, will decompose the object in the process of hashing it
decompose {bool} -- if True, will decompose the object in
the process of hashing it
Returns:
str -- the hash (id) of the fully serialized object
@@ -378,6 +538,7 @@ class Base(_RegisteringBase):
)
def _handle_object_count(self, obj: Any, parsed: List) -> int:
# pylint: disable=isinstance-second-argument-not-valid-type
count = 0
if obj is None:
return count
@@ -406,7 +567,7 @@ Base.update_forward_refs()
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: List[Any] = None
data: Union[List[Any], None] = None
def __init__(self) -> None:
super().__init__()
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Any, Callable, List, Type
from typing import Any, Callable, Dict, List, Optional, Type
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
@@ -43,8 +43,8 @@ def curve_from_list(args: List[float]):
class ObjectArray:
def __init__(self) -> None:
self.data = []
def __init__(self, data: Optional[list] = None) -> None:
self.data = data or []
@classmethod
def from_objects(cls, objects: List[Base]) -> "ObjectArray":
@@ -60,18 +60,17 @@ class ObjectArray:
"All objects in chunk should have the same speckle_type. "
f"Found {speckle_type} and {obj.speckle_type}"
)
data_list.encode_object(object=obj)
data_list.encode_object(obj=obj)
return data_list
@staticmethod
def decode_data(
data: List[Any], decoder: Callable[[List[Any]], Base]
data: List[Any], decoder: Callable[[List[Any]], Base], **kwargs: Dict[str, Any]
) -> List[Base]:
bases = []
bases: List[Base] = []
if not data:
return bases
index = 0
while index < len(data):
item_length = int(data[index])
@@ -79,19 +78,16 @@ class ObjectArray:
item_end = item_start + item_length
item_data = data[item_start:item_end]
index = item_end
# TODO: investigate what's going on w this fail
try:
decoded_data = decoder(item_data)
bases.append(decoded_data)
except ValueError:
continue
decoded_data = decoder(item_data, **kwargs)
bases.append(decoded_data)
return bases
def decode(self, decoder: Callable[[List[Any]], Any]):
return self.decode_data(data=self.data, decoder=decoder)
def decode(self, decoder: Callable[[List[Any]], Any], **kwargs: Dict[str, Any]):
return self.decode_data(data=self.data, decoder=decoder, **kwargs)
def encode_object(self, object: Base):
encoded = object.to_list()
def encode_object(self, obj: Base):
encoded = obj.to_list()
encoded.insert(0, len(encoded))
self.data.extend(encoded)
@@ -128,8 +124,7 @@ class CurveArray(ObjectArray):
@classmethod
def _curve_decoder(cls, data: List[float]) -> Base:
crv_array = cls()
crv_array.data = data
crv_array = cls(data)
return crv_array.to_curve()
def to_curves(self) -> List[Base]:
@@ -1,5 +1,6 @@
from enum import Enum
from typing import List
from typing import List, Optional
from specklepy.objects.geometry import Point
from .base import Base
@@ -16,8 +17,8 @@ DETACHABLE = {"detach_this", "origin", "detached_list"}
class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
pointslist: List[Base] = None
dots: List[int] = None
pointslist: Optional[List[Base]] = None
dots: Optional[List[int]] = None
class FakeDirection(Enum):
@@ -28,15 +29,15 @@ class FakeDirection(Enum):
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
textureCoordinates: List[float] = None
cardinal_dir: FakeDirection = None
test_bases: List[Base] = None
detach_this: Base = None
detached_list: List[Base] = None
_origin: Point = None
vertices: Optional[List[float]] = None
faces: Optional[List[int]] = None
colors: Optional[List[int]] = None
textureCoordinates: Optional[List[float]] = None
cardinal_dir: Optional[FakeDirection] = None
test_bases: Optional[List[Base]] = None
detach_this: Optional[Base] = None
detached_list: Optional[List[Base]] = None
_origin: Optional[Point] = None
# def __init__(self, **kwargs) -> None:
# super(FakeMesh, self).__init__(**kwargs)
+942
View File
@@ -0,0 +1,942 @@
from enum import Enum
from typing import Any, List, Optional
from specklepy.objects.base import Base
from specklepy.objects.encoding import CurveArray, CurveTypeEncoding, ObjectArray
from specklepy.objects.primitive import Interval
from specklepy.objects.units import get_encoding_from_units, get_units_from_encoding
GEOMETRY = "Objects.Geometry."
class Point(Base, speckle_type=GEOMETRY + "Point"):
x: float = 0.0
y: float = 0.0
z: float = 0.0
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id:"
f" {self.id}, speckle_type: {self.speckle_type})"
)
@classmethod
def from_list(cls, args: List[float]) -> "Point":
"""
Create a new Point from a list of three floats
representing the x, y, and z coordinates
"""
return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[Any]:
return [self.x, self.y, self.z]
@classmethod
def from_coords(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0):
"""Create a new Point from x, y, and z values"""
pt = Point()
pt.x, pt.y, pt.z = x, y, z
return pt
class Pointcloud(Base, speckle_type=GEOMETRY + "Pointcloud"):
points: Optional[List[float]] = None
colors: Optional[List[int]] = None
sizes: Optional[List[float]] = None
bbox: Optional["Box"] = None
class Vector(Base, speckle_type=GEOMETRY + "Vector"):
x: float = 0.0
y: float = 0.0
z: float = 0.0
applicationId: Optional[str] = None
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]) -> "Vector":
"""
Create from a list of three floats representing the x, y, and z coordinates.
"""
return cls(x=args[0], y=args[1], z=args[2])
def to_list(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) -> "Vector":
"""Create a new Point from x, y, and z values"""
v = Vector()
v.x, v.y, v.z = x, y, z
return v
class ControlPoint(Point, speckle_type=GEOMETRY + "ControlPoint"):
weight: Optional[float] = None
class Plane(Base, speckle_type=GEOMETRY + "Plane"):
origin: Point = Point()
normal: Vector = Vector()
xdir: Vector = Vector()
ydir: Vector = Vector()
@classmethod
def from_list(cls, args: List[Any]) -> "Plane":
return cls(
origin=Point.from_list(args[:3]),
normal=Vector.from_list(args[3:6]),
xdir=Vector.from_list(args[6:9]),
ydir=Vector.from_list(args[9:12]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
*self.origin.to_list(),
*self.normal.to_list(),
*self.xdir.to_list(),
*self.ydir.to_list(),
get_encoding_from_units(self._units),
]
class Box(Base, speckle_type=GEOMETRY + "Box"):
basePlane: Plane = Plane()
xSize: Interval = Interval()
ySize: Interval = Interval()
zSize: Interval = Interval()
area: Optional[float] = None
volume: Optional[float] = None
class Line(Base, speckle_type=GEOMETRY + "Line"):
start: Point = Point()
end: Optional[Point] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Line":
return cls(
start=Point.from_list(args[1:4]),
end=Point.from_list(args[4:7]),
domain=Interval.from_list(args[7:10]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
domain = self.domain.to_list() if self.domain else [0, 1]
return [
CurveTypeEncoding.Line.value,
*self.start.to_list(),
*self.end.to_list(),
*domain,
get_encoding_from_units(self._units),
]
class Arc(Base, speckle_type=GEOMETRY + "Arc"):
radius: Optional[float] = None
startAngle: Optional[float] = None
endAngle: Optional[float] = None
angleRadians: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
startPoint: Optional[Point] = None
midPoint: Optional[Point] = None
endPoint: Optional[Point] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[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]),
startPoint=Point.from_list(args[20:23]),
midPoint=Point.from_list(args[23:26]),
endPoint=Point.from_list(args[26:29]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Arc.value,
self.radius,
self.startAngle,
self.endAngle,
self.angleRadians,
*self.domain.to_list(),
*self.plane.to_list(),
*self.startPoint.to_list(),
*self.midPoint.to_list(),
*self.endPoint.to_list(),
get_encoding_from_units(self._units),
]
class Circle(Base, speckle_type=GEOMETRY + "Circle"):
radius: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[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]:
return [
CurveTypeEncoding.Circle.value,
self.radius,
*self.domain.to_list(),
*self.plane.to_list(),
get_encoding_from_units(self._units),
]
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
firstRadius: Optional[float] = None
secondRadius: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
trimDomain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[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]:
return [
CurveTypeEncoding.Ellipse.value,
self.firstRadius,
self.secondRadius,
*self.domain.to_list(),
*self.plane.to_list(),
get_encoding_from_units(self._units),
]
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
value: Optional[List[float]] = None
closed: Optional[bool] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_points(cls, points: List[Point]):
"""Create a new Polyline from a list of Points"""
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]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Polyline.value,
int(self.closed),
*self.domain.to_list(),
len(self.value),
*self.value,
get_encoding_from_units(self._units),
]
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.value:
return
if len(self.value) % 3:
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
]
class SpiralType(Enum):
Biquadratic = (0,)
BiquadraticParabola = (1,)
Bloss = (2,)
Clothoid = (3,)
Cosine = (4,)
Cubic = (5,)
CubicParabola = (6,)
Radioid = (7,)
Sinusoid = (8,)
Unknown = 9
class Spiral(Base, speckle_type=GEOMETRY + "Spiral", detachable={"displayValue"}):
startPoint: Optional[Point] = None
endPoint: Optional[Point]
plane: Optional[Plane]
turns: Optional[int]
pitchAxis: Optional[Vector] = Vector()
pitch: float = 0
spiralType: Optional[SpiralType] = None
displayValue: Optional[Polyline] = None
bbox: Optional[Box] = None
length: Optional[float] = None
domain: Optional[Interval] = None
class Curve(
Base,
speckle_type=GEOMETRY + "Curve",
chunkable={"points": 20000, "weights": 20000, "knots": 20000},
):
degree: Optional[int] = None
periodic: Optional[bool] = None
rational: Optional[bool] = None
points: Optional[List[float]] = None
weights: Optional[List[float]] = None
knots: Optional[List[float]] = None
domain: Optional[Interval] = None
displayValue: Optional[Polyline] = None
closed: Optional[bool] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.points:
return
if len(self.points) % 3:
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 = int(args[7])
weights_count = int(args[8])
knots_count = int(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=int(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]:
return [
CurveTypeEncoding.Curve.value,
self.degree,
int(self.periodic),
int(self.rational),
int(self.closed),
*self.domain.to_list(),
len(self.points),
len(self.weights),
len(self.knots),
*self.points,
*self.weights,
*self.knots,
get_encoding_from_units(self._units),
]
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: Optional[List[Base]] = None
domain: Optional[Interval] = None
closed: Optional[bool] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Polycurve":
curve_arrays = CurveArray(args[5:-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]:
curve_array = CurveArray.from_curves(self.segments).data
return [
CurveTypeEncoding.Polycurve.value,
int(self.closed),
*self.domain.to_list(),
len(curve_array),
*curve_array,
get_encoding_from_units(self._units),
]
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
capped: Optional[bool] = None
profile: Optional[Base] = None
pathStart: Optional[Point] = None
pathEnd: Optional[Point] = None
pathCurve: Optional[Base] = None
pathTangent: Optional[Base] = None
profiles: Optional[List[Base]] = None
length: Optional[float] = None
area: Optional[float] = None
volume: Optional[float] = None
bbox: Optional[Box] = None
class Mesh(
Base,
speckle_type=GEOMETRY + "Mesh",
chunkable={
"vertices": 2000,
"faces": 2000,
"colors": 2000,
"textureCoordinates": 2000,
},
):
vertices: Optional[List[float]] = None
faces: Optional[List[int]] = None
colors: Optional[List[int]] = None
textureCoordinates: Optional[List[float]] = None
bbox: Optional[Box] = None
area: Optional[float] = None
volume: Optional[float] = None
@classmethod
def create(
cls,
vertices: List[float],
faces: List[int],
colors: Optional[List[int]] = None,
texture_coordinates: Optional[List[float]] = None,
) -> "Mesh":
"""
Create a new Mesh from lists representing its vertices, faces,
colors (optional), and texture coordinates (optional).
This will initialise empty lists for colors and texture coordinates
if you do not provide any.
"""
return cls(
vertices=vertices,
faces=faces,
colors=colors or [],
textureCoordinates=texture_coordinates or [],
)
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: Optional[int] = None
degreeV: Optional[int] = None
rational: Optional[bool] = None
area: Optional[float] = None
pointData: Optional[List[float]] = None
countU: Optional[int] = None
countV: Optional[int] = None
bbox: Optional[Box] = None
closedU: Optional[bool] = None
closedV: Optional[bool] = None
domainU: Optional[Interval] = None
domainV: Optional[Interval] = None
knotsU: Optional[List[float]] = None
knotsV: Optional[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]:
return [
self.degreeU,
self.degreeV,
self.countU,
self.countV,
int(self.rational),
int(self.closedU),
int(self.closedV),
*self.domainU.to_list(),
*self.domainV.to_list(),
len(self.pointData),
len(self.knotsU),
len(self.knotsV),
*self.pointData,
*self.knotsU,
*self.knotsV,
get_encoding_from_units(self._units),
]
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
_Brep: Optional["Brep"] = None
SurfaceIndex: Optional[int] = None
OuterLoopIndex: Optional[int] = None
OrientationReversed: Optional[bool] = None
LoopIndices: Optional[List[int]] = None
@property
def _outer_loop(self):
return self._Brep.Loops[self.OuterLoopIndex] # pylint: disable=no-member
@property
def _surface(self):
return self._Brep.Surfaces[self.SurfaceIndex] # pylint: disable=no-member
@property
def _loops(self):
if self.LoopIndices:
# pylint: disable=not-an-iterable, no-member
return [self._Brep.Loops[i] for i in self.LoopIndices]
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepFace":
return cls(
_Brep=brep,
SurfaceIndex=args[0],
OuterLoopIndex=args[1],
OrientationReversed=bool(args[2]),
LoopIndices=args[3:],
)
def to_list(self) -> List[Any]:
return [
self.SurfaceIndex,
self.OuterLoopIndex,
int(self.OrientationReversed),
*self.LoopIndices,
]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
_Brep: Optional["Brep"] = None
Curve3dIndex: Optional[int] = None
TrimIndices: Optional[List[int]] = None
StartIndex: Optional[int] = None
EndIndex: Optional[int] = None
ProxyCurveIsReversed: Optional[bool] = None
Domain: Optional[Interval] = None
@property
def _start_vertex(self):
return self._Brep.Vertices[self.StartIndex]
@property
def _end_vertex(self):
return self._Brep.Vertices[self.EndIndex]
@property
def _trims(self):
if self.TrimIndices:
# pylint: disable=not-an-iterable
return [self._Brep.Trims[i] for i in self.TrimIndices]
@property
def _curve(self):
return self._Brep.Curve3D[self.Curve3dIndex]
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepEdge":
domain_start = args[4]
domain_end = args[5]
domain = (
Interval(start=domain_start, end=domain_end)
if None not in (domain_start, domain_end)
else None
)
return cls(
_Brep=brep,
Curve3dIndex=int(args[0]),
TrimIndices=[int(t) for t in args[6:]],
StartIndex=int(args[1]),
EndIndex=int(args[2]),
ProxyCurveIsReversed=bool(args[3]),
Domain=domain,
)
def to_list(self) -> List[Any]:
return [
self.Curve3dIndex,
self.StartIndex,
self.EndIndex,
int(self.ProxyCurveIsReversed),
self.Domain.start,
self.Domain.end,
*self.TrimIndices,
]
class BrepLoopType(int, Enum):
Unknown = 0
Outer = 1
Inner = 2
Slit = 3
CurveOnSurface = 4
PointOnSurface = 5
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
_Brep: Optional["Brep"] = None
FaceIndex: Optional[Optional[int]] = None
TrimIndices: Optional[List[int]] = None
Type: Optional[BrepLoopType] = None
@property
def _face(self):
return self._Brep.Faces[self.FaceIndex]
@property
def _trims(self):
if self.TrimIndices:
# pylint: disable=not-an-iterable
return [self._Brep.Trims[i] for i in self.TrimIndices]
@classmethod
def from_list(cls, args: List[any], brep: "Brep" = None):
return cls(
_Brep=brep,
FaceIndex=args[0],
Type=BrepLoopType(args[1]),
TrimIndices=args[2:],
)
def to_list(self) -> List[int]:
return [
self.FaceIndex,
self.Type.value,
*self.TrimIndices,
]
class BrepTrimType(int, Enum):
Unknown = 0
Boundary = 1
Mated = 2
Seam = 3
Singular = 4
CurveOnSurface = 5
PointOnSurface = 6
Slit = 7
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
_Brep: Optional["Brep"] = None
EdgeIndex: Optional[int] = None
StartIndex: Optional[int] = None
EndIndex: Optional[int] = None
FaceIndex: Optional[int] = None
LoopIndex: Optional[int] = None
CurveIndex: Optional[int] = None
IsoStatus: Optional[int] = None
TrimType: Optional[BrepTrimType] = None
IsReversed: Optional[bool] = None
Domain: Optional[Interval] = None
@property
def _face(self):
if self._Brep:
return self._Brep.Faces[self.FaceIndex] # pylint: disable=no-member
@property
def _loop(self):
if self._Brep:
return self._Brep.Loops[self.LoopIndex] # pylint: disable=no-member
@property
def _edge(self):
if self._Brep:
# pylint: disable=no-member
return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
@property
def _curve_2d(self):
if self._Brep:
return self._Brep.Curve2D[self.CurveIndex] # pylint: disable=no-member
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepTrim":
return cls(
_Brep=brep,
EdgeIndex=args[0],
StartIndex=args[1],
EndIndex=args[2],
FaceIndex=args[3],
LoopIndex=args[4],
CurveIndex=args[5],
IsoStatus=args[6],
TrimType=BrepTrimType(args[7]),
IsReversed=bool(args[8]),
)
def to_list(self) -> List[Any]:
return [
self.EdgeIndex,
self.StartIndex,
self.EndIndex,
self.FaceIndex,
self.LoopIndex,
self.CurveIndex,
self.IsoStatus,
self.TrimType.value,
int(self.IsReversed),
]
class Brep(
Base,
speckle_type=GEOMETRY + "Brep",
chunkable={
"SurfacesValue": 31250,
"Curve3DValues": 31250,
"Curve2DValues": 31250,
"VerticesValue": 31250,
"EdgesValue": 62500,
"LoopsValue": 62500,
"FacesValue": 62500,
"TrimsValue": 62500,
},
detachable={"displayValue"},
serialize_ignore={
"Surfaces",
"Curve3D",
"Curve2D",
"Vertices",
"Trims",
"Edges",
"Loops",
"Faces",
},
):
provenance: Optional[str] = None
bbox: Optional[Box] = None
area: Optional[float] = None
volume: Optional[float] = None
_displayValue: Optional[List[Mesh]] = None
Surfaces: Optional[List[Surface]] = None
Curve3D: Optional[List[Base]] = None
Curve2D: Optional[List[Base]] = None
Vertices: Optional[List[Point]] = None
Edges: Optional[List[BrepEdge]] = None
Loops: Optional[List[BrepLoop]] = None
Faces: Optional[List[BrepFace]] = None
Trims: Optional[List[BrepTrim]] = None
IsClosed: Optional[bool] = None
Orientation: Optional[int] = None
def _inject_self_into_children(self, children: Optional[List[Base]]) -> List[Base]:
if children is None:
return children
for child in children:
child._Brep = self # pylint: disable=protected-access
return children
# set as prop for now for backwards compatibility
@property
def displayValue(self) -> List[Mesh]:
return self._displayValue
@displayValue.setter
def displayValue(self, value):
if isinstance(value, Mesh):
self._displayValue = [value]
elif isinstance(value, list):
self._displayValue = value
@property
def EdgesValue(self) -> List[BrepEdge]:
return None if self.Edges is None else ObjectArray.from_objects(self.Edges).data
@EdgesValue.setter
def EdgesValue(self, value: List[float]):
if not value:
return
self.Edges = ObjectArray.decode_data(value, BrepEdge.from_list, brep=self)
@property
def LoopsValue(self) -> List[BrepLoop]:
return None if self.Loops is None else ObjectArray.from_objects(self.Loops).data
@LoopsValue.setter
def LoopsValue(self, value: List[int]):
if not value:
return
self.Loops = ObjectArray.decode_data(value, BrepLoop.from_list, brep=self)
@property
def FacesValue(self) -> List[int]:
return None if self.Faces is None else ObjectArray.from_objects(self.Faces).data
@FacesValue.setter
def FacesValue(self, value: List[int]):
if not value:
return
self.Faces = ObjectArray.decode_data(value, BrepFace.from_list, brep=self)
@property
def SurfacesValue(self) -> List[float]:
return (
None
if self.Surfaces is None
else ObjectArray.from_objects(self.Surfaces).data
)
@SurfacesValue.setter
def SurfacesValue(self, value: List[float]):
if not value:
return
self.Surfaces = ObjectArray.decode_data(value, Surface.from_list)
@property
def Curve3DValues(self) -> List[float]:
return (
None if self.Curve3D is None else CurveArray.from_curves(self.Curve3D).data
)
@Curve3DValues.setter
def Curve3DValues(self, value: List[float]):
crv_array = CurveArray(value)
self.Curve3D = crv_array.to_curves()
@property
def Curve2DValues(self) -> List[Base]:
return (
None if self.Curve2D is None else CurveArray.from_curves(self.Curve2D).data
)
@Curve2DValues.setter
def Curve2DValues(self, value: List[float]):
crv_array = CurveArray(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
# TODO: can this be consistent with loops, edges, faces, curves, etc and prepend with the chunk list? needs to happen in sharp first
@property
def TrimsValue(self) -> List[float]:
# return None if self.Trims is None else ObjectArray.from_objects(self.Trims).data
if not self.Trims:
return
value = []
for trim in self.Trims:
value.extend(trim.to_list())
return value
@TrimsValue.setter
def TrimsValue(self, value: List[float]):
if not value:
return
# self.Trims = ObjectArray.decode_data(value, BrepTrim.from_list, brep=self)
self.Trims = [
BrepTrim.from_list(value[i : i + 9], self) for i in range(0, len(value), 9)
]
BrepEdge.update_forward_refs()
BrepLoop.update_forward_refs()
BrepTrim.update_forward_refs()
BrepFace.update_forward_refs()
@@ -1,5 +1,7 @@
from typing import Any, List
from typing import Any, List, Optional
from specklepy.objects.geometry import Point, Vector
from .base import Base
OTHER = "Objects.Other."
@@ -24,8 +26,23 @@ IDENTITY_TRANSFORM = [
]
class Material(Base, speckle_type=OTHER + "Material"):
"""Generic class for materials containing generic parameters."""
name: Optional[str] = None
class RevitMaterial(Material, speckle_type="Objects.Other.Revit." + "RevitMaterial"):
materialCategory: Optional[str] = None
materialClass: Optional[str] = None
shininess: Optional[int] = None
smoothness: Optional[int] = None
transparency: Optional[int] = None
parameters: Optional[Base] = None
class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
name: str = None
name: Optional[str] = None
opacity: float = 1
metalness: float = 0
roughness: float = 1
@@ -33,6 +50,25 @@ class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
emissive: int = -16777216 # black arbg
class MaterialQuantity(Base, speckle_type=OTHER + "MaterialQuantity"):
material: Optional[Material] = None
volume: Optional[float] = None
area: Optional[float] = None
class DisplayStyle(Base, speckle_type=OTHER + "DisplayStyle"):
"""
Minimal display style class.
Developed primarily for display styles in Rhino and AutoCAD.
Rhino object attributes uses OpenNURBS definition for linetypes and lineweights.
"""
name: Optional[str] = None
color: int = -2894893 # light gray arbg
linetype: Optional[str] = None
lineweight: float = 0
class Transform(
Base,
speckle_type=OTHER + "Transform",
@@ -41,10 +77,11 @@ class Transform(
"""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).
The 4th column defines translation,
where the last value is a divisor (usually equal to 1).
"""
_value: List[float] = None
_value: Optional[List[float]] = None
@property
def value(self) -> List[float]:
@@ -57,12 +94,14 @@ class Transform(
value = [float(x) for x in value]
except (ValueError, TypeError) as error:
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}"
"Could not create a Transform object with the requested value. Input"
f" must be a 16 element list of numbers. Value provided: {value}"
) from error
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"
"Could not create a Transform object: input list should be 16 floats"
f" long, but was {len(value)} long"
)
self._value = value
@@ -127,14 +166,16 @@ class Transform(
"""Transform a list of speckle Points
Arguments:
points {List[float]} -- a flat list of floats representing points to transform
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"
"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):
@@ -171,11 +212,13 @@ class Transform(
][: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.
def from_list(cls, value: Optional[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)
value {List[float]} -- the matrix as a flat list of 16 numbers
(defaults to the identity transform)
Returns:
Transform -- a complete transform object
@@ -188,27 +231,27 @@ class Transform(
class BlockDefinition(
Base, speckle_type=OTHER + "BlockDefinition", detachable={"geometry"}
):
name: str = None
basePoint: Point = None
geometry: List[Base] = None
name: Optional[str] = None
basePoint: Optional[Point] = None
geometry: Optional[List[Base]] = None
class BlockInstance(
Base, speckle_type=OTHER + "BlockInstance", detachable={"blockDefinition"}
):
blockDefinition: BlockDefinition = None
transform: Transform = None
blockDefinition: Optional[BlockDefinition] = None
transform: Optional[Transform] = None
# TODO: prob move this into a built elements module, but just trialling this for now
class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter"):
name: str = None
name: Optional[str] = None
value: Any = None
applicationUnitType: str = None # eg UnitType UT_Length
applicationUnit: str = None # DisplayUnitType eg DUT_MILLIMITERS
applicationInternalName: str = (
None # BuiltInParameterName or GUID for shared parameter
)
applicationUnitType: Optional[str] = None # eg UnitType UT_Length
applicationUnit: Optional[str] = None # DisplayUnitType eg DUT_MILLIMITERS
applicationInternalName: Optional[
str
] = None # BuiltInParameterName or GUID for shared parameter
isShared: bool = False
isReadOnly: bool = False
isTypeParameter: bool = False
+25
View File
@@ -0,0 +1,25 @@
from typing import Any, List
from specklepy.objects.base import Base
NAMESPACE = "Objects.Primitive"
class Interval(Base, speckle_type=f"{NAMESPACE}.Interval"):
start: float = 0.0
end: float = 0.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 Interval2d(Base, speckle_type=f"{NAMESPACE}.Interval2d"):
u: Interval
v: Interval
@@ -0,0 +1,141 @@
"""Builtin Speckle object kit."""
from specklepy.objects.structural.analysis import (
Model,
ModelInfo,
ModelSettings,
ModelUnits,
)
from specklepy.objects.structural.axis import Axis
from specklepy.objects.structural.geometry import (
Element1D,
Element2D,
Element3D,
ElementType1D,
ElementType2D,
ElementType3D,
Node,
Restraint,
)
from specklepy.objects.structural.loading import (
ActionType,
BeamLoadType,
CombinationType,
FaceLoadType,
Load,
LoadAxisType,
LoadBeam,
LoadCase,
LoadCombinations,
LoadDirection,
LoadDirection2D,
LoadFace,
LoadGravity,
LoadNode,
LoadType,
)
from specklepy.objects.structural.materials import (
Concrete,
MaterialType,
Steel,
StructuralMaterial,
Timber,
)
from specklepy.objects.structural.properties import (
BaseReferencePoint,
MemberType,
Property,
Property1D,
Property2D,
Property3D,
PropertyDamper,
PropertyMass,
PropertySpring,
PropertyType2D,
PropertyType3D,
PropertyTypeDamper,
PropertyTypeSpring,
ReferenceSurface,
ReferenceSurfaceEnum,
SectionProfile,
ShapeType,
shapeType,
)
from specklepy.objects.structural.results import (
Result,
Result1D,
Result2D,
Result3D,
ResultGlobal,
ResultNode,
ResultSet1D,
ResultSet2D,
ResultSet3D,
ResultSetAll,
ResultSetNode,
)
__all__ = [
"Element1D",
"Element2D",
"Element3D",
"ElementType1D",
"ElementType2D",
"ElementType3D",
"Axis",
"Node",
"Restraint",
"Load",
"LoadType",
"ActionType",
"BeamLoadType",
"FaceLoadType",
"LoadDirection",
"LoadDirection2D",
"LoadAxisType",
"CombinationType",
"LoadBeam",
"LoadCase",
"LoadCombinations",
"LoadFace",
"LoadGravity",
"LoadNode",
"Model",
"ModelInfo",
"ModelSettings",
"ModelUnits",
"MaterialType",
"Concrete",
"StructuralMaterial",
"Steel",
"Timber",
"Property",
"Property1D",
"Property2D",
"Property3D",
"PropertyDamper",
"PropertyMass",
"PropertySpring",
"SectionProfile",
"MemberType",
"BaseReferencePoint",
"ReferenceSurface",
"PropertyType2D",
"PropertyType3D",
"ShapeType",
"PropertyTypeSpring",
"PropertyTypeDamper",
"ReferenceSurfaceEnum",
"shapeType",
"Result",
"Result1D",
"ResultSet1D",
"Result2D",
"ResultSet2D",
"Result3D",
"ResultSet3D",
"ResultGlobal",
"ResultSetNode",
"ResultNode",
"ResultSetAll",
]
@@ -0,0 +1,49 @@
from typing import List, Optional
from specklepy.objects.base import Base
STRUCTURAL_ANALYSIS = "Objects.Structural.Analysis."
class ModelUnits(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelUnits"):
length: Optional[str] = None
sections: Optional[str] = None
displacements: Optional[str] = None
stress: Optional[str] = None
force: Optional[str] = None
mass: Optional[str] = None
time: Optional[str] = None
temperature: Optional[str] = None
velocity: Optional[str] = None
acceleration: Optional[str] = None
energy: Optional[str] = None
angle: Optional[str] = None
strain: Optional[str] = None
class ModelSettings(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelSettings"):
modelUnits: Optional[ModelUnits] = None
steelCode: Optional[str] = None
concreteCode: Optional[str] = None
coincidenceTolerance: float = 0.0
class ModelInfo(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelInfo"):
name: Optional[str] = None
description: Optional[str] = None
projectNumber: Optional[str] = None
projectName: Optional[str] = None
settings: Optional[ModelSettings] = None
initials: Optional[str] = None
application: Optional[str] = None
class Model(Base, speckle_type=STRUCTURAL_ANALYSIS + "Model"):
specs: Optional[ModelInfo] = None
nodes: Optional[List] = None
elements: Optional[List] = None
loads: Optional[List] = None
restraints: Optional[List] = None
properties: Optional[List] = None
materials: Optional[List] = None
layerDescription: Optional[str] = None
+10
View File
@@ -0,0 +1,10 @@
from typing import Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry import Plane
class Axis(Base, speckle_type="Objects.Structural.Geometry.Axis"):
name: Optional[str] = None
axisType: Optional[str] = None
plane: Optional[Plane] = None
@@ -0,0 +1,110 @@
from enum import Enum
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry import Line, Mesh, Plane, Point, Vector
from specklepy.objects.structural.axis import Axis
from specklepy.objects.structural.properties import (
Property1D,
Property2D,
Property3D,
PropertyDamper,
PropertyMass,
PropertySpring,
)
STRUCTURAL_GEOMETRY = "Objects.Structural.Geometry"
class ElementType1D(int, Enum):
Beam = 0
Brace = 1
Bar = 2
Column = 3
Rod = 4
Spring = 5
Tie = 6
Strut = 7
Link = 8
Damper = 9
Cable = 10
Spacer = 11
Other = 12
Null = 13
class ElementType2D(int, Enum):
Quad4 = 0
Quad8 = 1
Triangle3 = 2
Triangle6 = 3
class ElementType3D(int, Enum):
Brick8 = 0
Wedge6 = 1
Pyramid5 = 2
Tetra4 = 3
class Restraint(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Restraint"):
code: Optional[str] = None
stiffnessX: float = 0.0
stiffnessY: float = 0.0
stiffnessZ: float = 0.0
stiffnessXX: float = 0.0
stiffnessYY: float = 0.0
stiffnessZZ: float = 0.0
class Node(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Node"):
name: Optional[str] = None
basePoint: Optional[Point] = None
constraintAxis: Optional[Axis] = None
restraint: Optional[Restraint] = None
springProperty: Optional[PropertySpring] = None
massProperty: Optional[PropertyMass] = None
damperProperty: Optional[PropertyDamper] = None
class Element1D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element1D"):
name: Optional[str] = None
baseLine: Optional[Line] = None
property: Optional[Property1D] = None
type: Optional[ElementType1D] = None
end1Releases: Optional[Restraint] = None
end2Releases: Optional[Restraint] = None
end1Offset: Optional[Vector] = None
end2Offset: Optional[Vector] = None
orientationNode: Optional[Node] = None
orinetationAngle: float = 0.0
localAxis: Optional[Plane] = None
parent: Optional[Base] = None
end1Node: Optional[Node] = None
end2Node: Optional[Node] = None
topology: Optional[List] = None
displayMesh: Optional[Mesh] = None
class Element2D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element2D"):
name: Optional[str] = None
property: Optional[Property2D] = None
type: Optional[ElementType2D] = None
offset: float = 0.0
orientationAngle: float = 0.0
parent: Optional[Base] = None
topology: Optional[List] = None
displayMesh: Optional[Mesh] = None
class Element3D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element3D"):
name: Optional[str] = None
baseMesh: Optional[Mesh] = None
property: Optional[Property3D] = None
type: Optional[ElementType3D] = None
orientationAngle: float = 0.0
parent: Optional[Base] = None
topology: List
# class Storey needs ependency on built elements first
@@ -1,14 +1,14 @@
from enum import Enum
from typing import List
from typing import List, Optional
from ..base import Base
from .geometry import *
from specklepy.objects.base import Base
from specklepy.objects.geometry import Vector
from specklepy.objects.structural.axis import Axis
STRUCTURAL_LOADING = "Objects.Structural.Loading."
class LoadType(int, Enum):
none = 0
Dead = 1
SuperDead = 2
@@ -31,7 +31,6 @@ class LoadType(int, Enum):
class ActionType(int, Enum):
none = 0
Permanent = 1
Variable = 2
@@ -39,7 +38,6 @@ class ActionType(int, Enum):
class BeamLoadType(int, Enum):
Point = 0
Uniform = 1
Linear = 2
@@ -48,21 +46,18 @@ class BeamLoadType(int, Enum):
class FaceLoadType(int, Enum):
Constant = 0
Variable = 1
Point = 2
class LoadDirection2D(int, Enum):
X = 0
Y = 1
Z = 2
class LoadDirection(int, Enum):
X = 0
Y = 1
Z = 2
@@ -80,7 +75,6 @@ class LoadAxisType(int, Enum):
class CombinationType(int, Enum):
LinearAdd = 0
Envelope = 1
AbsoluteAdd = 2
@@ -89,56 +83,55 @@ class CombinationType(int, Enum):
class LoadCase(Base, speckle_type=STRUCTURAL_LOADING + "LoadCase"):
name: str = None
loadType: LoadType = None
group: str = None
actionType: ActionType = None
description: str = None
name: Optional[str] = None
loadType: Optional[LoadType] = None
group: Optional[str] = None
actionType: Optional[ActionType] = None
description: Optional[str] = None
class Load(Base, speckle_type=STRUCTURAL_LOADING + "Load"):
name: str = None
units: str = None
loadCase: LoadCase = None
name: Optional[str] = None
loadCase: Optional[LoadCase] = None
class LoadBeam(Load, speckle_type=STRUCTURAL_LOADING + "LoadBeam"):
elements: List = None
loadType: BeamLoadType = None
direction: LoadDirection = None
loadAxis: Axis = None
loadAxisType: LoadAxisType = None
isProjected: bool = None
values: List = None
positions: List = None
elements: Optional[List] = None
loadType: Optional[BeamLoadType] = None
direction: Optional[LoadDirection] = None
loadAxis: Optional[Axis] = None
loadAxisType: Optional[LoadAxisType] = None
isProjected: Optional[bool] = None
values: Optional[List] = None
positions: Optional[List] = None
class LoadCombinations(Base, speckle_type=STRUCTURAL_LOADING + "LoadCombination"):
name: str = None
name: Optional[str] = None
loadCases: List
loadFactors: List
combinationType: CombinationType
class LoadFace(Load, speckle_type=STRUCTURAL_LOADING + "LoadFace"):
elements: List = None
loadType: FaceLoadType = None
direction: LoadDirection2D = None
loadAxis: Axis = None
loadAxisType: LoadAxisType = None
isProjected: bool = None
values: List = None
positions: List = None
elements: Optional[List] = None
loadType: Optional[FaceLoadType] = None
direction: Optional[LoadDirection2D] = None
loadAxis: Optional[Axis] = None
loadAxisType: Optional[LoadAxisType] = None
isProjected: Optional[bool] = None
values: Optional[List] = None
positions: Optional[List] = None
class LoadGravity(Load, speckle_type=STRUCTURAL_LOADING + "LoadGravity"):
elements: List = None
nodes: List = None
gravityFactors: Vector = None
elements: Optional[List] = None
nodes: Optional[List] = None
gravityFactors: Optional[Vector] = None
class LoadNode(Load, speckle_type=STRUCTURAL_LOADING + "LoadNode"):
nodes: List = None
loadAxis: Axis = None
direction: LoadDirection = None
nodes: Optional[List] = None
loadAxis: Optional[Axis] = None
direction: Optional[LoadDirection] = None
value: float = 0.0
@@ -1,7 +1,7 @@
from enum import Enum
from typing import Optional
from ..base import Base
from specklepy.objects.base import Base
STRUCTURAL_MATERIALS = "Objects.Structural.Materials"
@@ -21,12 +21,14 @@ class MaterialType(int, Enum):
Other = 11
class Material(Base, speckle_type=STRUCTURAL_MATERIALS):
name: str = None
grade: str = None
materialType: MaterialType = None
designCode: str = None
codeYear: str = None
class StructuralMaterial(
Base, speckle_type=STRUCTURAL_MATERIALS + ".StructuralMaterial"
):
name: Optional[str] = None
grade: Optional[str] = None
materialType: Optional[MaterialType] = None
designCode: Optional[str] = None
codeYear: Optional[str] = None
strength: float = 0.0
elasticModulus: float = 0.0
poissonsRatio: float = 0.0
@@ -38,22 +40,22 @@ class Material(Base, speckle_type=STRUCTURAL_MATERIALS):
materialSafetyFactor: float = 0.0
class Concrete(Material, speckle_type=STRUCTURAL_MATERIALS + ".Concrete"):
class Concrete(StructuralMaterial):
compressiveStrength: float = 0.0
tensileStrength: float = 0.0
flexuralStrength: float = 0.0
maxCompressiveStrength: float = 0.0
maxTensileStrength: float = 0.0
maxCompressiveStrain: float = 0.0
maxTensileStrain: float = 0.0
maxAggregateSize: float = 0.0
lightweight: bool = None
lightweight: Optional[bool] = None
class Steel(Material, speckle_type=STRUCTURAL_MATERIALS + ".Steel"):
class Steel(StructuralMaterial, speckle_type=STRUCTURAL_MATERIALS + ".Steel"):
yieldStrength: float = 0.0
ultimateStrength: float = 0.0
maxStrain: float = 0.0
strainHardeningModulus: float = 0.0
class Timber(Material, speckle_type=STRUCTURAL_MATERIALS + ".Timber"):
species: str = None
class Timber(StructuralMaterial, speckle_type=STRUCTURAL_MATERIALS + ".Timber"):
species: Optional[str] = None
@@ -1,12 +1,11 @@
from enum import Enum
from typing import Optional
from ..base import Base
from specklepy.objects.base import Base
from specklepy.objects.structural.axis import Axis
from specklepy.objects.structural.materials import StructuralMaterial
from .material import *
from .axis import Axis
STRUCTURAL_PROPERTY = "Objectives.Structural.Properties"
STRUCTURAL_PROPERTY = "Objects.Structural.Properties"
class MemberType(int, Enum):
@@ -39,7 +38,6 @@ class ReferenceSurface(int, Enum):
class PropertyType2D(int, Enum):
Stress = 0
Fabric = 1
Plate = 2
@@ -59,7 +57,7 @@ class PropertyType3D(int, Enum):
class ShapeType(int, Enum):
Rectangular = 0
Circular = 1
I = 2
I = 2 # noqa: E741
Tee = 3
Angle = 4
Channel = 5
@@ -89,36 +87,35 @@ class PropertyTypeDamper(int, Enum):
class Property(Base, speckle_type=STRUCTURAL_PROPERTY):
name: str = None
name: Optional[str] = None
class SectionProfile(Base, speckle_type=STRUCTURAL_PROPERTY + ".SectionProfile"):
name: str = None
shapeType: ShapeType = None
name: Optional[str] = None
shapeType: Optional[ShapeType] = None
area: float = 0.0
Iyy: float = 0.0
Izz: float = 0.0
J: float = 0.0
Ky: float = 0.0
weight: float = 0.0
units: str = None
class Property1D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property1D"):
memberType: MemberType = None
Material: Material = None
SectionProfile: SectionProfile = None
BaseReferencePoint: BaseReferencePoint = None
memberType: Optional[MemberType] = None
material: Optional[StructuralMaterial] = None
profile: Optional[SectionProfile] = None
referencePoint: Optional[BaseReferencePoint] = None
offsetY: float = 0.0
offsetZ: float = 0.0
class Property2D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property2D"):
PropertyType2D: PropertyType2D = None
type: Optional[PropertyType2D] = None
thickness: float = 0.0
Material: Material = None
axis: Axis = None
referenceSurface: ReferenceSurface = None
material: Optional[StructuralMaterial] = None
orientationAxis: Optional[Axis] = None
refSurface: Optional[ReferenceSurface] = None
zOffset: float = 0.0
modifierInPlane: float = 0.0
modifierBending: float = 0.0
@@ -127,13 +124,13 @@ class Property2D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property2D"):
class Property3D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property3D"):
PropertyType3D: PropertyType3D = None
Material: Material = None
axis: Axis = None
type: Optional[PropertyType3D] = None
material: Optional[StructuralMaterial] = None
orientationAxis: Optional[Axis] = None
class PropertyDamper(Property, speckle_type=STRUCTURAL_PROPERTY + ".PropertyDamper"):
damperType: PropertyTypeDamper = None
damperType: Optional[PropertyTypeDamper] = None
dampingX: float = 0.0
dampingY: float = 0.0
dampingZ: float = 0.0
@@ -150,14 +147,14 @@ class PropertyMass(Property, speckle_type=STRUCTURAL_PROPERTY + ".PropertyMass")
inertiaXY: float = 0.0
inertiaYZ: float = 0.0
inertiaZX: float = 0.0
massModified: bool = None
massModified: Optional[bool] = None
massModifierX: float = 0.0
massModifierY: float = 0.0
massModifierZ: float = 0.0
class PropertySpring(Property, speckle_type=STRUCTURAL_PROPERTY + ".PropertySpring"):
springType: PropertyTypeSpring = None
springType: Optional[PropertyTypeSpring] = None
springCurveX: float = 0.0
stiffnessX: float = 0.0
springCurveY: float = 0.0
+172
View File
@@ -0,0 +1,172 @@
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.structural.analysis import Model
from specklepy.objects.structural.geometry import Element1D, Element2D, Element3D, Node
STRUCTURAL_RESULTS = "Objects.Structural.Results."
class Result(Base, speckle_type=STRUCTURAL_RESULTS + "Result"):
resultCase: Optional[Base] = None
permutation: Optional[str] = None
description: Optional[str] = None
class ResultSet1D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet1D"):
results1D: List
class Result1D(Result, speckle_type=STRUCTURAL_RESULTS + "Result1D"):
element: Optional[Element1D] = None
position: Optional[float] = None
dispX: Optional[float] = None
dispY: Optional[float] = None
dispZ: Optional[float] = None
rotXX: Optional[float] = None
rotYY: Optional[float] = None
rotZZ: Optional[float] = None
forceX: Optional[float] = None
forceY: Optional[float] = None
forceZ: Optional[float] = None
momentXX: Optional[float] = None
momentYY: Optional[float] = None
momentZZ: Optional[float] = None
axialStress: Optional[float] = None
shearStressY: Optional[float] = None
shearStressZ: Optional[float] = None
bendingStressYPos: Optional[float] = None
bendingStressYNeg: Optional[float] = None
bendingStressZPos: Optional[float] = None
bendingStressZNeg: Optional[float] = None
combinedStressMax: Optional[float] = None
combinedStressMin: Optional[float] = None
class ResultSet2D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet2D"):
results2D: List
class Result2D(Result, speckle_type=STRUCTURAL_RESULTS + "Result2D"):
element: Optional[Element2D] = None
position: List
dispX: Optional[float] = None
dispY: Optional[float] = None
dispZ: Optional[float] = None
forceXX: Optional[float] = None
forceYY: Optional[float] = None
forceXY: Optional[float] = None
momentXX: Optional[float] = None
momentYY: Optional[float] = None
momentXY: Optional[float] = None
shearX: Optional[float] = None
shearY: Optional[float] = None
stressTopXX: Optional[float] = None
stressTopYY: Optional[float] = None
stressTopZZ: Optional[float] = None
stressTopXY: Optional[float] = None
stressTopYZ: Optional[float] = None
stressTopZX: Optional[float] = None
stressMidXX: Optional[float] = None
stressMidYY: Optional[float] = None
stressMidZZ: Optional[float] = None
stressMidXY: Optional[float] = None
stressMidYZ: Optional[float] = None
stressMidZX: Optional[float] = None
stressBotXX: Optional[float] = None
stressBotYY: Optional[float] = None
stressBotZZ: Optional[float] = None
stressBotXY: Optional[float] = None
stressBotYZ: Optional[float] = None
stressBotZX: Optional[float] = None
class ResultSet3D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet3D"):
results3D: List
class Result3D(Result, speckle_type=STRUCTURAL_RESULTS + "Result3D"):
element: Optional[Element3D] = None
position: List
dispX: Optional[float] = None
dispY: Optional[float] = None
dispZ: Optional[float] = None
stressXX: Optional[float] = None
stressYY: Optional[float] = None
stressZZ: Optional[float] = None
stressXY: Optional[float] = None
stressYZ: Optional[float] = None
stressZX: Optional[float] = None
class ResultGlobal(Result, speckle_type=STRUCTURAL_RESULTS + "ResultGlobal"):
model: Optional[Model] = None
loadX: Optional[float] = None
loadY: Optional[float] = None
loadZ: Optional[float] = None
loadXX: Optional[float] = None
loadYY: Optional[float] = None
loadZZ: Optional[float] = None
reactionX: Optional[float] = None
reactionY: Optional[float] = None
reactionZ: Optional[float] = None
reactionXX: Optional[float] = None
reactionYY: Optional[float] = None
reactionZZ: Optional[float] = None
mode: Optional[float] = None
frequency: Optional[float] = None
loadFactor: Optional[float] = None
modalStiffness: Optional[float] = None
modalGeoStiffness: Optional[float] = None
effMassX: Optional[float] = None
effMassY: Optional[float] = None
effMassZ: Optional[float] = None
effMassXX: Optional[float] = None
effMassYY: Optional[float] = None
effMassZZ: Optional[float] = None
class ResultSetNode(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSetNode"):
resultsNode: List
class ResultNode(Result, speckle_type=STRUCTURAL_RESULTS + " ResultNode"):
node: Optional[Node] = None
dispX: Optional[float] = None
dispY: Optional[float] = None
dispZ: Optional[float] = None
rotXX: Optional[float] = None
rotYY: Optional[float] = None
rotZZ: Optional[float] = None
reactionX: Optional[float] = None
reactionY: Optional[float] = None
reactionZ: Optional[float] = None
reactionXX: Optional[float] = None
reactionYY: Optional[float] = None
reactionZZ: Optional[float] = None
constraintX: Optional[float] = None
constraintY: Optional[float] = None
constraintZ: Optional[float] = None
constraintXX: Optional[float] = None
constraintYY: Optional[float] = None
constraintZZ: Optional[float] = None
velX: Optional[float] = None
velY: Optional[float] = None
velZ: Optional[float] = None
velXX: Optional[float] = None
velYY: Optional[float] = None
velZZ: Optional[float] = None
accX: Optional[float] = None
accY: Optional[float] = None
accZ: Optional[float] = None
accXX: Optional[float] = None
accYY: Optional[float] = None
accZZ: Optional[float] = None
class ResultSetAll(Base, speckle_type=None):
resultSet1D: Optional[ResultSet1D] = None
resultSet2D: Optional[ResultSet2D] = None
resultSet3D: Optional[ResultSet3D] = None
resultsGlobal: Optional[ResultGlobal] = None
resultsNode: Optional[ResultSetNode] = None
+84
View File
@@ -0,0 +1,84 @@
from enum import Enum
from typing import Union
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
__all__ = [
"Units",
"get_encoding_from_units",
"get_units_from_encoding",
"get_units_from_string",
]
class Units(Enum):
mm = "mm"
cm = "cm"
m = "m"
km = "km"
inches = "in"
feet = "ft"
yards = "yd"
miles = "mi"
none = "none"
UNITS_STRINGS = {
Units.mm: ["mm", "mil", "millimeters", "millimetres"],
Units.cm: ["cm", "centimetre", "centimeter", "centimetres", "centimeters"],
Units.m: ["m", "meter", "meters", "metre", "metres"],
Units.km: ["km", "kilometer", "kilometre", "kilometers", "kilometres"],
Units.inches: ["in", "inch", "inches"],
Units.feet: ["ft", "foot", "feet"],
Units.yards: ["yd", "yard", "yards"],
Units.miles: ["mi", "mile", "miles"],
Units.none: ["none", "null"],
}
UNITS_ENCODINGS = {
Units.none: 0,
None: 0,
Units.mm: 1,
Units.cm: 2,
Units.m: 3,
Units.km: 4,
Units.inches: 5,
Units.feet: 6,
Units.yards: 7,
Units.miles: 8,
}
def get_units_from_string(unit: str) -> Units:
if not isinstance(unit, str):
raise SpeckleInvalidUnitException(unit)
unit = str.lower(unit)
for name, alternates in UNITS_STRINGS.items():
if unit in alternates:
return name
raise SpeckleInvalidUnitException(unit)
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."
f"Please enter a valid unit encoding (eg {UNITS_ENCODINGS})."
)
)
def get_encoding_from_units(unit: Union[Units, None]):
try:
return UNITS_ENCODINGS[unit]
except KeyError as e:
raise SpeckleException(
message=(
f"No encoding exists for unit {unit}."
f"Please enter a valid unit to encode (eg {UNITS_ENCODINGS})."
)
) from e
@@ -1,21 +1,17 @@
import re
import ujson
import hashlib
import re
import warnings
from uuid import uuid4
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple
from uuid import uuid4
from warnings import warn
from typing import Any, Dict, List, Tuple
from specklepy.objects.base import Base, DataChunk
from specklepy.logging.exceptions import (
SpeckleException,
SpeckleWarning,
)
from specklepy.transports.abstract_transport import AbstractTransport
import ujson
# import for serialization
import specklepy.objects.geometry
import specklepy.objects.other
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.objects.base import Base, DataChunk
from specklepy.transports.abstract_transport import AbstractTransport
PRIMITIVES = (int, float, str, bool)
@@ -31,7 +27,8 @@ def safe_json_loads(obj: str, obj_id=None) -> Any:
import json
warn(
f"Failed to deserialise object (id: {obj_id}). This is likely a ujson big int error - falling back to json. \nError: {err}",
f"Failed to deserialise object (id: {obj_id}). This is likely a ujson big"
f" int error - falling back to json. \nError: {err}",
SpeckleWarning,
)
return json.loads(obj)
@@ -40,16 +37,26 @@ def safe_json_loads(obj: str, obj_id=None) -> Any:
class BaseObjectSerializer:
read_transport: AbstractTransport
write_transports: List[AbstractTransport]
detach_lineage: List[bool] = [] # tracks depth and whether or not to detach
lineage: List[str] = [] # keeps track of hash chain through the object tree
family_tree: Dict[str, Dict[str, int]] = {}
closure_table: Dict[str, Dict[str, int]] = {}
detach_lineage: List[bool] # tracks depth and whether or not to detach
lineage: List[str] # keeps track of hash chain through the object tree
family_tree: Dict[str, Dict[str, int]]
closure_table: Dict[str, Dict[str, int]]
deserialized: Dict[
str, Base
] # holds deserialized objects so objects with same id return the same instance
def __init__(
self, write_transports: List[AbstractTransport] = [], read_transport=None
self,
write_transports: Optional[List[AbstractTransport]] = None,
read_transport: Optional[AbstractTransport] = None,
) -> None:
self.write_transports = write_transports
self.write_transports = write_transports or []
self.read_transport = read_transport
self.detach_lineage = []
self.lineage = []
self.family_tree = {}
self.closure_table = {}
self.deserialized = {}
def write_json(self, base: Base):
"""Serializes a given base object into a json string
@@ -57,21 +64,23 @@ class BaseObjectSerializer:
base {Base} -- the base object to be decomposed and serialized
Returns:
(str, str) -- a tuple containing the hash (id) of the base object and the serialized object string
(str, str) -- a tuple containing the object id of the base object and
the serialized object string
"""
hash, obj = self.traverse_base(base)
obj_id, obj = self.traverse_base(base)
return hash, ujson.dumps(obj)
return obj_id, ujson.dumps(obj)
def traverse_base(self, base: Base) -> Tuple[str, Dict]:
def traverse_base(self, base: Base) -> Tuple[str, Dict[str, Any]]:
"""Decomposes the given base object and builds a serializable dictionary
Arguments:
base {Base} -- the base object to be decomposed and serialized
Returns:
(str, dict) -- a tuple containing the hash (id) of the base object and the constructed serializable dictionary
(str, dict) -- a tuple containing the object id of the base object and
the constructed serializable dictionary
"""
self.__reset_writer()
@@ -79,13 +88,13 @@ class BaseObjectSerializer:
for wt in self.write_transports:
wt.begin_write()
hash, obj = self._traverse_base(base)
obj_id, obj = self._traverse_base(base)
if self.write_transports:
for wt in self.write_transports:
wt.end_write()
return hash, obj
return obj_id, obj
def _traverse_base(self, base: Base) -> Tuple[str, Dict]:
if not self.detach_lineage:
@@ -110,11 +119,6 @@ class BaseObjectSerializer:
if prop == "id":
continue
# allow serialisation of nulls
if value is None:
object_builder[prop] = value
continue
# only bother with chunking and detaching if there is a write transport
if self.write_transports:
dynamic_chunk_match = prop.startswith("@") and re.match(
@@ -131,8 +135,8 @@ class BaseObjectSerializer:
prop.startswith("@") or prop in base._detachable or chunkable
)
# 1. handle primitives (ints, floats, strings, and bools)
if isinstance(value, PRIMITIVES):
# 1. handle None and primitives (ints, floats, strings, and bools)
if value is None or isinstance(value, PRIMITIVES):
object_builder[prop] = value
continue
@@ -145,8 +149,8 @@ class BaseObjectSerializer:
elif isinstance(value, Base):
child_obj = self.traverse_value(value, detach=detach)
if detach and self.write_transports:
ref_hash = child_obj["id"]
object_builder[prop] = self.detach_helper(ref_hash=ref_hash)
ref_id = child_obj["id"]
object_builder[prop] = self.detach_helper(ref_id=ref_id)
else:
object_builder[prop] = child_obj
@@ -165,8 +169,8 @@ class BaseObjectSerializer:
chunk_refs = []
for c in chunks:
self.detach_lineage.append(detach)
ref_hash, _ = self._traverse_base(c)
ref_obj = self.detach_helper(ref_hash=ref_hash)
ref_id, _ = self._traverse_base(c)
ref_obj = self.detach_helper(ref_id=ref_id)
chunk_refs.append(ref_obj)
object_builder[prop] = chunk_refs
@@ -185,20 +189,20 @@ class BaseObjectSerializer:
}
object_builder["totalChildrenCount"] = len(closure)
hash = hash_obj(object_builder)
obj_id = hash_obj(object_builder)
object_builder["id"] = hash
object_builder["id"] = obj_id
if closure:
object_builder["__closure"] = self.closure_table[hash] = closure
object_builder["__closure"] = self.closure_table[obj_id] = closure
# 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=obj_id, serialized_object=ujson.dumps(object_builder))
del self.lineage[-1]
return hash, object_builder
return obj_id, object_builder
def traverse_value(self, obj: Any, detach: bool = False) -> Any:
"""Decomposes a given object and constructs a serializable object or dictionary
@@ -209,6 +213,8 @@ class BaseObjectSerializer:
Returns:
Any -- a serializable version of the given object
"""
if obj is None:
return None
if isinstance(obj, PRIMITIVES):
return obj
@@ -224,8 +230,8 @@ class BaseObjectSerializer:
for o in obj:
if isinstance(o, Base):
self.detach_lineage.append(detach)
hash, _ = self._traverse_base(o)
detached_list.append(self.detach_helper(ref_hash=hash))
ref_id, _ = self._traverse_base(o)
detached_list.append(self.detach_helper(ref_id=ref_id))
else:
detached_list.append(self.traverse_value(o, detach))
return detached_list
@@ -246,19 +252,22 @@ class BaseObjectSerializer:
else:
try:
return obj.dict()
except:
except Exception:
warn(
f"Failed to handle {type(obj)} in `BaseObjectSerializer.traverse_value`",
f"Failed to handle {type(obj)} in"
" `BaseObjectSerializer.traverse_value`",
SpeckleWarning,
)
return str(obj)
def detach_helper(self, ref_hash: str) -> Dict[str, str]:
"""Helper to keep track of detached objects and their depth in the family tree and create reference objects to place in the parent object
def detach_helper(self, ref_id: str) -> Dict[str, str]:
"""
Helper to keep track of detached objects and their depth in the family tree
and create reference objects to place in the parent object
Arguments:
ref_hash {str} -- the hash of the fully traversed object
ref_id {str} -- the id of the fully traversed object
Returns:
dict -- a reference object to be inserted into the given object's parent
@@ -267,18 +276,21 @@ class BaseObjectSerializer:
for parent in self.lineage:
if parent not in self.family_tree:
self.family_tree[parent] = {}
if ref_hash not in self.family_tree[parent] or self.family_tree[parent][
ref_hash
if ref_id not in self.family_tree[parent] or self.family_tree[parent][
ref_id
] > len(self.detach_lineage):
self.family_tree[parent][ref_hash] = len(self.detach_lineage)
self.family_tree[parent][ref_id] = len(self.detach_lineage)
return {
"referencedId": ref_hash,
"referencedId": ref_id,
"speckle_type": "reference",
}
def __reset_writer(self) -> None:
"""Reinitializes the lineage, and other variables that get used during the json writing process"""
"""
Reinitializes the lineage, and other variables that get used during the json
writing process
"""
self.detach_lineage = [True]
self.lineage = []
self.family_tree = {}
@@ -295,6 +307,8 @@ class BaseObjectSerializer:
"""
if not obj_string:
return None
self.deserialized = {}
obj = safe_json_loads(obj_string)
return self.recompose_base(obj=obj)
@@ -313,6 +327,9 @@ class BaseObjectSerializer:
if isinstance(obj, str):
obj = safe_json_loads(obj)
if "id" in obj and obj["id"] in self.deserialized:
return self.deserialized[obj["id"]]
if "speckle_type" in obj and obj["speckle_type"] == "reference":
obj = self.get_child(obj=obj)
@@ -343,14 +360,15 @@ class BaseObjectSerializer:
# 2. handle referenced child objects
elif "referencedId" in value:
ref_hash = value["referencedId"]
ref_obj_str = self.read_transport.get_object(id=ref_hash)
ref_id = value["referencedId"]
ref_obj_str = self.read_transport.get_object(id=ref_id)
if ref_obj_str:
ref_obj = safe_json_loads(ref_obj_str, ref_hash)
ref_obj = safe_json_loads(ref_obj_str, ref_id)
base.__setattr__(prop, self.recompose_base(obj=ref_obj))
else:
warnings.warn(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}",
f"Could not find the referenced child object of id `{ref_id}`"
f" in the given read transport: {self.read_transport.name}",
SpeckleWarning,
)
base.__setattr__(prop, self.handle_value(value))
@@ -359,10 +377,14 @@ class BaseObjectSerializer:
else:
base.__setattr__(prop, self.handle_value(value))
if "id" in obj:
self.deserialized[obj["id"]] = base
return base
def handle_value(self, obj: Any):
"""Helper for recomposing a base object by handling the dictionary representation's values
"""Helper for recomposing a base object by handling the dictionary
representation's values
Arguments:
obj {Any} -- a value from the base object dictionary
@@ -404,13 +426,14 @@ class BaseObjectSerializer:
return obj
def get_child(self, obj: Dict):
ref_hash = obj["referencedId"]
ref_obj_str = self.read_transport.get_object(id=ref_hash)
ref_id = obj["referencedId"]
ref_obj_str = self.read_transport.get_object(id=ref_id)
if not ref_obj_str:
warnings.warn(
f"Could not find the referenced child object of id `{ref_hash}` in the given read transport: {self.read_transport.name}",
f"Could not find the referenced child object of id `{ref_id}` in the"
f" given read transport: {self.read_transport.name}",
SpeckleWarning,
)
return obj
return safe_json_loads(ref_obj_str, ref_hash)
return safe_json_loads(ref_obj_str, ref_id)
@@ -1,16 +1,8 @@
from abc import ABC, abstractmethod
from typing import Optional, List, Dict
from pydantic import BaseModel
from pydantic.main import Extra
from typing import Dict, List, Optional
# __________________
# | |
# | this is v wip |
# | pls be careful |
# |__________________|
# (\__/) ||
# (•ㅅ•) ||
# /  
from pydantic import BaseModel
from pydantic.config import Extra
class AbstractTransport(ABC, BaseModel):
@@ -27,7 +19,9 @@ class AbstractTransport(ABC, BaseModel):
@abstractmethod
def end_write(self) -> None:
"""Optional: signals to the transport that no more items will need to be written."""
"""
Optional: signals to the transport that no more items will need to be written.
"""
pass
@abstractmethod
@@ -48,7 +42,8 @@ class AbstractTransport(ABC, BaseModel):
Arguments:
id {str} -- the hash of the object
source_transport {AbstractTransport) -- the transport through which the object can be found
source_transport {AbstractTransport)
-- the transport through which the object can be found
"""
pass
@@ -60,7 +55,8 @@ class AbstractTransport(ABC, BaseModel):
id {str} -- the hash of the object
Returns:
str -- the full string representation of the object (or null if no object is found)
str -- the full string representation
of the object (or null if no object is found)
"""
pass
@@ -72,7 +68,8 @@ class AbstractTransport(ABC, BaseModel):
id_list -- List of object id to be checked
Returns:
Dict[str, bool] -- keys: input ids, values: whether the transport has that object
Dict[str, bool] -- keys: input ids, values:
whether the transport has that object
"""
pass
@@ -84,7 +81,8 @@ class AbstractTransport(ABC, BaseModel):
Arguments:
id {str} -- the id of the object you want to copy
target_transport {AbstractTransport} -- the transport you want to copy the object to
target_transport {AbstractTransport}
-- the transport you want to copy the object to
Returns:
str -- the string representation of the root object
"""
@@ -1,4 +1,5 @@
from typing import Any, List, Dict
from typing import Any, Dict, List
from specklepy.transports.abstract_transport import AbstractTransport
@@ -0,0 +1,3 @@
from specklepy.transports.server.server import ServerTransport
__all__ = ["ServerTransport"]
@@ -1,10 +1,11 @@
import gzip
import json
import logging
import threading
import queue
import gzip
import threading
import requests
from specklepy.logging.exceptions import SpeckleException
LOG = logging.getLogger(__name__)
@@ -107,7 +108,8 @@ class BatchSender(object):
if not new_objects:
LOG.info(
f"Uploading batch of {len(batch)} objects: all objects are already in the server"
f"Uploading batch of {len(batch)} objects: all objects are already in"
" the server"
)
return
@@ -126,11 +128,16 @@ class BatchSender(object):
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}"
message=(
"Could not save the object to the server - status code"
f" {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.",
f"Failed to send objects to {self.server_url}. Please ensure this"
f" stream ({self.stream_id}) exists on this server and that you have"
" permission to send to it.",
error,
)
@@ -1,8 +1,8 @@
import json
import requests
from typing import Any, Dict, List, Optional
from warnings import warn
from typing import Any, Dict, List
import requests
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import Account, get_account_from_token
@@ -14,10 +14,10 @@ from .batch_sender import BatchSender
class ServerTransport(AbstractTransport):
"""
The `ServerTransport` is the vehicle through which you transport objects to and from a Speckle Server. Provide it to
`operations.send()` or `operations.receive()`.
The `ServerTransport` 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:
The `ServerTransport` can be authenticated two different ways:
1. by providing a `SpeckleClient`
2. by providing an `Account`
3. by providing a `token` and `url`
@@ -29,14 +29,15 @@ class ServerTransport(AbstractTransport):
# 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
# 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(
commit_id = client.commit.create(
stream_id=new_stream_id,
obj_id=hash,
message="this is a block I made in speckle-py",
@@ -45,25 +46,26 @@ class ServerTransport(AbstractTransport):
"""
_name = "RemoteTransport"
url: str = None
stream_id: str = None
account: Account = None
url: Optional[str] = None
stream_id: Optional[str] = None
account: Optional[Account] = None
saved_obj_count: int = 0
session: requests.Session = None
session: Optional[requests.Session] = None
def __init__(
self,
stream_id: str,
client: SpeckleClient = None,
account: Account = None,
token: str = None,
url: str = None,
client: Optional[SpeckleClient] = None,
account: Optional[Account] = None,
token: Optional[str] = None,
url: Optional[str] = None,
**data: Any,
) -> None:
super().__init__(**data)
if client is None and account is None and token is None and url is None:
raise SpeckleException(
"You must provide either a client or a token and url to construct a ServerTransport."
"You must provide either a client or a token and url to construct a"
" ServerTransport."
)
if account:
@@ -74,7 +76,8 @@ class ServerTransport(AbstractTransport):
if not client.account.token:
warn(
SpeckleWarning(
f"Unauthenticated Speckle Client provided to Server Transport for {self.url}. Receiving from private streams will fail."
"Unauthenticated Speckle Client provided to Server Transport"
f" for {self.url}. Receiving from private streams will fail."
)
)
else:
@@ -118,8 +121,10 @@ class ServerTransport(AbstractTransport):
# return obj
raise SpeckleException(
"Getting a single object using `ServerTransport.get_object()` is not implemented. To get an object from the server, please use the `SpeckleClient.object.get()` route",
NotImplementedError,
"Getting a single object using `ServerTransport.get_object()` is not"
" implemented. To get an object from the server, please use the"
" `SpeckleClient.object.get()` route",
NotImplementedError(),
)
def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
@@ -134,7 +139,8 @@ class ServerTransport(AbstractTransport):
if r.status_code != 200:
raise SpeckleException(
f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})"
f"Can't get object {self.stream_id}/{id}: HTTP error"
f" {r.status_code} ({r.text[:1000]})"
)
root_obj_serialized = r.text
root_obj = json.loads(root_obj_serialized)
@@ -156,29 +162,13 @@ class ServerTransport(AbstractTransport):
lines = r.iter_lines(decode_unicode=True)
# iter through returned objects saving them as we go
target_transport.begin_write()
for line in lines:
if line:
hash, obj = line.split("\t")
target_transport.save_object(hash, obj)
target_transport.save_object(id, root_obj_serialized)
target_transport.end_write()
return root_obj_serialized
# async def stream_res(self, endpoint: str) -> str:
# data = b""
# async with aiohttp.ClientSession() as session:
# session.headers.update(
# {
# "Authorization": f"{self.session.headers['Authorization']}",
# "Accept": "text/plain",
# }
# )
# async with session.get(endpoint) as res:
# while True:
# chunk = await res.content.read(self.chunk_size)
# if not chunk:
# break
# data += chunk
# return data.decode("utf-8")
@@ -1,32 +1,30 @@
import os
import sys
import time
import sched
import sqlite3
from typing import Any, List, Dict, Tuple
from appdirs import user_data_dir
from contextlib import closing
from specklepy.transports.abstract_transport import AbstractTransport
from typing import Any, Dict, List, Optional, Tuple
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
class SQLiteTransport(AbstractTransport):
_name = "SQLite"
_base_path: str = None
_root_path: str = None
__connection: sqlite3.Connection = None
_base_path: Optional[str] = None
_root_path: Optional[str] = None
__connection: Optional[sqlite3.Connection] = None
app_name: str = ""
scope: str = ""
saved_obj_count: int = 0
max_size: int = None
_current_batch: List[Tuple[str, str]] = None
_current_batch_size: int = None
max_size: Optional[int] = None
_current_batch: Optional[List[Tuple[str, str]]] = None
_current_batch_size: Optional[int] = None
def __init__(
self,
base_path: str = None,
app_name: str = None,
scope: str = None,
base_path: Optional[str] = None,
app_name: Optional[str] = None,
scope: Optional[str] = None,
max_batch_size_mb: float = 10.0,
**data: Any,
) -> None:
@@ -47,7 +45,9 @@ class SQLiteTransport(AbstractTransport):
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.",
f"SQLiteTransport could not initialise {self.scope}.db at"
f" {self._base_path}. Either provide a different `base_path` or use an"
" alternative transport.",
ex,
)
@@ -56,21 +56,25 @@ class SQLiteTransport(AbstractTransport):
@staticmethod
def get_base_path(app_name):
# 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
if system.startswith("java"):
import platform
# # 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
# if system.startswith("java"):
# import platform
os_name = platform.java_ver()[3][0]
if os_name.startswith("Mac"):
system = "darwin"
# os_name = platform.java_ver()[3][0]
# if os_name.startswith("Mac"):
# system = "darwin"
if system != "darwin":
return user_data_dir(appname=app_name, appauthor=False, roaming=True)
# if system != "darwin":
# return user_data_dir(appname=app_name, appauthor=False, roaming=True)
path = os.path.expanduser("~/.config/")
return os.path.join(path, app_name)
# path = os.path.expanduser("~/.config/")
# return os.path.join(path, app_name)
return str(
speckle_path_provider.user_application_data_path().joinpath(app_name)
)
def save_object_from_transport(
self, id: str, source_transport: AbstractTransport
@@ -79,14 +83,16 @@ class SQLiteTransport(AbstractTransport):
Arguments:
id {str} -- the object id
source_transport {AbstractTransport) -- the transport through which the object can be found
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)
def save_object(self, id: str, serialized_object: str) -> None:
"""
Adds an object to the current batch to be written to the db. If the current batch is full,
Adds an object to the current batch to be written to the db.
If the current batch is full,
the batch is written to the db and the current batch is reset.
Arguments:
@@ -118,7 +124,8 @@ class SQLiteTransport(AbstractTransport):
self.__connection.commit()
except Exception as ex:
raise SpeckleException(
f"Could not save the batch of objects to the local db. Inner exception: {ex}",
"Could not save the batch of objects to the local db. Inner exception:"
f" {ex}",
ex,
)
@@ -157,7 +164,10 @@ class SQLiteTransport(AbstractTransport):
raise NotImplementedError
def get_all_objects(self):
"""Returns all the objects in the store. NOTE: do not use for large collections!"""
"""
Returns all the objects in the store.
NOTE: do not use for large collections!
"""
self.__check_connection()
with closing(self.__connection.cursor()) as c:
rows = c.execute("SELECT * FROM objects").fetchall()
@@ -188,4 +198,4 @@ class SQLiteTransport(AbstractTransport):
self.__connection = sqlite3.connect(self._root_path)
def __del__(self):
self.__connection.close()
self.close()
+2 -95
View File
@@ -1,101 +1,8 @@
import uuid
import random
import pytest
import requests
from specklepy.api.models import Stream
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.objects.geometry import Point
from specklepy.objects.fakemesh import FakeDirection, FakeMesh
from specklepy.logging import metrics
metrics.disable()
@pytest.fixture(scope="session")
def host():
return "localhost:3000"
def seed_user(host):
seed = uuid.uuid4().hex
user_dict = {
"email": f"{seed[0:7]}@spockle.com",
"password": "$uper$3cr3tP@ss",
"name": f"{seed[0:7]} Name",
"company": "test spockle",
}
r = requests.post(
url=f"http://{host}/auth/local/register?challenge=pyspeckletests",
data=user_dict,
)
print(r.url)
access_code = r.url.split("access_code=")[1]
r_tokens = requests.post(
url=f"http://{host}/auth/token",
json={
"appSecret": "spklwebapp",
"appId": "spklwebapp",
"accessCode": access_code,
"challenge": "pyspeckletests",
},
)
user_dict.update(**r_tokens.json())
return user_dict
@pytest.fixture(scope="session")
def user_dict(host):
return seed_user(host)
@pytest.fixture(scope="session")
def second_user_dict(host):
return seed_user(host)
@pytest.fixture(scope="session")
def client(host, user_dict):
client = SpeckleClient(host=host, use_ssl=False)
client.authenticate_with_token(user_dict["token"])
return client
@pytest.fixture(scope="session")
def sample_stream(client):
stream = Stream(
name="a sample stream for testing",
description="a stream created for testing",
isPublic=True,
)
stream.id = client.stream.create(stream.name, stream.description, stream.isPublic)
return stream
@pytest.fixture(scope="session")
def mesh():
mesh = FakeMesh()
mesh.name = "my_mesh"
mesh.vertices = [random.uniform(0, 10) for _ in range(1, 210)]
mesh.faces = list(range(1, 210))
mesh["@(100)colours"] = [random.uniform(0, 10) for _ in range(1, 210)]
mesh["@()default_chunk"] = [random.uniform(0, 10) for _ in range(1, 210)]
mesh.cardinal_dir = FakeDirection.WEST
mesh.test_bases = [Base(name=f"test {i}") for i in range(1, 22)]
mesh.detach_this = Base(name="predefined detached base")
mesh["@detach"] = Base(name="detached base")
mesh["@detached_list"] = [
42,
"some text",
[1, 2, 3],
Base(name="detached within a list"),
]
mesh.origin = Point(x=4, y=2)
return mesh
@pytest.fixture(scope="session")
View File
+112
View File
@@ -0,0 +1,112 @@
import random
import uuid
import pytest
import requests
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Stream
from specklepy.logging import metrics
from specklepy.objects.base import Base
from specklepy.objects.fakemesh import FakeDirection, FakeMesh
from specklepy.objects.geometry import Point
metrics.disable()
@pytest.fixture(scope="session")
def host():
return "localhost:3000"
def seed_user(host):
seed = uuid.uuid4().hex
user_dict = {
"email": f"{seed[0:7]}@spockle.com",
"password": "$uper$3cr3tP@ss",
"name": f"{seed[0:7]} Name",
"company": "test spockle",
}
r = requests.post(
url=f"http://{host}/auth/local/register?challenge=pyspeckletests",
data=user_dict,
# do not follow redirects here, they lead to the frontend, which might not be
# running in a test environment
# causing the response to not be OK in the end
allow_redirects=False,
)
if not r.ok:
raise Exception(f"Cannot seed user: {r.reason}")
access_code = r.text.split("access_code=")[1]
r_tokens = requests.post(
url=f"http://{host}/auth/token",
json={
"appSecret": "spklwebapp",
"appId": "spklwebapp",
"accessCode": access_code,
"challenge": "pyspeckletests",
},
)
user_dict.update(**r_tokens.json())
return user_dict
@pytest.fixture(scope="session")
def user_dict(host):
return seed_user(host)
@pytest.fixture(scope="session")
def second_user_dict(host):
return seed_user(host)
@pytest.fixture(scope="session")
def client(host, user_dict):
client = SpeckleClient(host=host, use_ssl=False)
client.authenticate_with_token(user_dict["token"])
return client
@pytest.fixture(scope="session")
def second_client(host, second_user_dict):
client = SpeckleClient(host=host, use_ssl=False)
client.authenticate_with_token(second_user_dict["token"])
return client
@pytest.fixture(scope="session")
def sample_stream(client):
stream = Stream(
name="a sample stream for testing",
description="a stream created for testing",
isPublic=True,
)
stream.id = client.stream.create(stream.name, stream.description, stream.isPublic)
return stream
@pytest.fixture(scope="session")
def mesh():
mesh = FakeMesh()
mesh.name = "my_mesh"
mesh.vertices = [random.uniform(0, 10) for _ in range(1, 210)]
mesh.faces = list(range(1, 210))
mesh["@(100)colours"] = [random.uniform(0, 10) for _ in range(1, 210)]
mesh["@()default_chunk"] = [random.uniform(0, 10) for _ in range(1, 210)]
mesh.cardinal_dir = FakeDirection.WEST
mesh.test_bases = [Base(name=f"test {i}") for i in range(1, 22)]
mesh.detach_this = Base(name="predefined detached base")
mesh["@detach"] = Base(name="detached base")
mesh["@detached_list"] = [
42,
"some text",
[1, 2, 3],
Base(name="detached within a list"),
]
mesh.origin = Point(x=4, y=2)
return mesh
+46
View File
@@ -0,0 +1,46 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Activity, ActivityCollection, User
from specklepy.logging.exceptions import SpeckleException
@pytest.mark.run(order=2)
class TestUser:
def test_user_get_self(self, client: SpeckleClient, user_dict):
fetched_user = client.active_user.get()
assert isinstance(fetched_user, User)
assert fetched_user.name == user_dict["name"]
assert fetched_user.email == user_dict["email"]
user_dict["id"] = fetched_user.id
def test_user_update(self, client: SpeckleClient):
bio = "i am a ghost in the machine"
failed_update = client.active_user.update()
assert isinstance(failed_update, SpeckleException)
updated = client.active_user.update(bio=bio)
me = client.active_user.get()
assert updated is True
assert me.bio == bio
def test_user_activity(self, client: SpeckleClient, second_user_dict):
my_activity = client.active_user.activity(limit=10)
their_activity = client.other_user.activity(second_user_dict["id"])
assert isinstance(my_activity, ActivityCollection)
assert my_activity.items
assert isinstance(my_activity.items[0], Activity)
assert my_activity.totalCount
assert isinstance(their_activity, ActivityCollection)
older_activity = client.user.activity(before=my_activity.items[0].time)
assert isinstance(older_activity, ActivityCollection)
assert older_activity.totalCount
assert older_activity.totalCount < my_activity.totalCount
@@ -1,7 +1,8 @@
import pytest
from specklepy.api import operations
from specklepy.transports.server import ServerTransport
from specklepy.api.models import Branch, Commit, Stream
from specklepy.transports.server import ServerTransport
class TestBranch:
@@ -1,18 +1,28 @@
from tempfile import gettempdir
import pytest
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.objects.base import Base
from specklepy.transports.server import ServerTransport
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
def test_invalid_authentication():
# overriding the appdata path (it would be enough to just override the Accounts)
# but this way its cleaner
speckle_path_provider.override_application_data_path(gettempdir())
client = SpeckleClient()
with pytest.warns(SpeckleWarning):
client.authenticate_with_token("fake token")
# remove path override
speckle_path_provider.override_application_data_path(None)
def test_invalid_send():
client = SpeckleClient()
@@ -45,4 +55,4 @@ def test_account_from_token_and_url():
acct = get_account_from_token(token, url)
assert acct.token == token
assert acct.serverInfo.url == url
assert acct.serverInfo.url == url
@@ -1,10 +1,11 @@
import pytest
from specklepy.api import operations
from specklepy.api.models import Commit, Stream
from specklepy.transports.server.server import ServerTransport
@pytest.mark.run(order=4)
@pytest.mark.run(order=6)
class TestCommit:
@pytest.fixture(scope="module")
def commit(self):
@@ -84,4 +85,4 @@ class TestCommit:
message="testing received",
)
assert commit_marked_received == True
assert commit_marked_received is True
@@ -1,4 +1,5 @@
import pytest
from specklepy.api.models import Stream
from specklepy.objects import Base
from specklepy.objects.encoding import ObjectArray
+49
View File
@@ -0,0 +1,49 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Activity, ActivityCollection, LimitedUser
@pytest.mark.run(order=4)
class TestOtherUser:
def test_user_get_self(self, client):
"""
Test, that a limited user query cannot query the active user.
"""
with pytest.raises(TypeError):
client.other_user.get()
def test_user_search(self, client, second_user_dict):
search_results = client.other_user.search(
search_query=second_user_dict["name"][:5]
)
assert isinstance(search_results, list)
assert len(search_results) > 0
result_user = search_results[0]
assert isinstance(result_user, LimitedUser)
assert result_user.name == second_user_dict["name"]
second_user_dict["id"] = result_user.id
assert getattr(result_user, "email", None) is None
def test_user_get(self, client, second_user_dict):
fetched_user = client.other_user.get(id=second_user_dict["id"])
assert isinstance(fetched_user, LimitedUser)
assert fetched_user.name == second_user_dict["name"]
# changed in the server, now you cannot get emails of other users
# not checking this, since the first user could or could not be an admin on the server
# admins can get emails of others, regular users can't
# assert fetched_user.email == None
second_user_dict["id"] = fetched_user.id
def test_user_activity(self, client: SpeckleClient, second_user_dict):
their_activity = client.other_user.activity(second_user_dict["id"])
assert isinstance(their_activity, ActivityCollection)
assert isinstance(their_activity.items, list)
assert isinstance(their_activity.items[0], Activity)
assert their_activity.totalCount
assert their_activity.totalCount > 0
@@ -1,15 +1,16 @@
import json
import pytest
from specklepy.api import operations
from specklepy.transports.server import ServerTransport
from specklepy.transports.memory import MemoryTransport
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.objects import Base
from specklepy.objects.geometry import Point
from specklepy.objects.fakemesh import FakeMesh
from specklepy.objects.geometry import Point
from specklepy.transports.memory import MemoryTransport
from specklepy.transports.server import ServerTransport
@pytest.mark.run(order=3)
@pytest.mark.run(order=5)
class TestSerialization:
def test_serialize(self, base):
serialized = operations.serialize(base)
@@ -91,7 +92,7 @@ class TestSerialization:
assert deserialised == {"foo": "bar"}
def test_big_int(self):
big_int = '{"big": ' + str(2 ** 64) + "}"
big_int = '{"big": ' + str(2**64) + "}"
deserialised = operations.deserialize(big_int)
assert deserialised == {"big": 2 ** 64}
assert deserialised == {"big": 2**64}
@@ -1,4 +1,6 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.api.models import ServerInfo
@@ -12,12 +14,22 @@ class TestServer:
"lifespan": 9001,
}
def test_server_get(self, client):
def test_server_get(self, client: SpeckleClient):
server = client.server.get()
assert isinstance(server, ServerInfo)
def test_server_apps(self, client):
def test_server_version(self, client: SpeckleClient):
version = client.server.version()
assert isinstance(version, tuple)
if len(version) == 1:
assert version[0] == "dev"
else:
assert isinstance(version[0], int)
assert len(version) >= 3
def test_server_apps(self, client: SpeckleClient):
apps = client.server.apps()
assert isinstance(apps, list)
+229
View File
@@ -0,0 +1,229 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.api.models import (
Activity,
ActivityCollection,
PendingStreamCollaborator,
Stream,
User,
)
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
UnsupportedException,
)
@pytest.mark.run(order=3)
class TestStream:
@pytest.fixture(scope="session")
def stream(self):
return Stream(
name="a wonderful stream",
description="a stream created for testing",
isPublic=True,
)
@pytest.fixture(scope="module")
def updated_stream(
self,
):
return Stream(
name="a wonderful updated stream",
description="an updated stream description for testing",
isPublic=False,
)
@pytest.fixture(scope="module")
def second_user(self, second_client: SpeckleClient):
return second_client.user.get()
def test_stream_create(self, client, stream, updated_stream):
stream.id = updated_stream.id = client.stream.create(
name=stream.name,
description=stream.description,
is_public=stream.isPublic,
)
assert isinstance(stream.id, str)
def test_stream_get(self, client, stream):
fetched_stream = client.stream.get(stream.id)
assert fetched_stream.name == stream.name
assert fetched_stream.description == stream.description
assert fetched_stream.isPublic == stream.isPublic
def test_stream_update(self, client, updated_stream):
updated = client.stream.update(
id=updated_stream.id,
name=updated_stream.name,
description=updated_stream.description,
is_public=updated_stream.isPublic,
)
fetched_stream = client.stream.get(updated_stream.id)
assert updated is True
assert fetched_stream.name == updated_stream.name
assert fetched_stream.description == updated_stream.description
assert fetched_stream.isPublic == updated_stream.isPublic
def test_stream_list(self, client):
client.stream.create(name="a second wonderful stream")
client.stream.create(name="a third fantastic stream")
streams = client.stream.list()
assert len(streams) >= 3
def test_stream_search(self, client, updated_stream):
search_results = client.stream.search(updated_stream.name)
assert len(search_results) == 1
assert search_results[0].name == updated_stream.name
def test_stream_favorite(self, client, stream):
favorited = client.stream.favorite(stream.id)
assert isinstance(favorited, Stream)
assert favorited.favoritedDate is not None
unfavorited = client.stream.favorite(stream.id, False)
assert isinstance(favorited, Stream)
assert unfavorited.favoritedDate is None
def test_stream_grant_permission(self, client, stream, second_user):
# deprecated as of Speckle Server 2.6.4
with pytest.raises(UnsupportedException):
client.stream.grant_permission(
stream_id=stream.id,
user_id=second_user.id,
role="stream:contributor",
)
def test_stream_invite(
self, client: SpeckleClient, stream: Stream, second_user_dict: dict
):
invited = client.stream.invite(
stream_id=stream.id,
email=second_user_dict["email"],
role="stream:reviewer",
message="welcome to my stream!",
)
assert invited is True
# fail if no email or id
with pytest.raises(SpeckleException):
client.stream.invite(stream_id=stream.id)
def test_stream_invite_get_all_for_user(
self, second_client: SpeckleClient, stream: Stream
):
# NOTE: these are user queries, but testing here to contain the flow
invites = second_client.user.get_all_pending_invites()
assert isinstance(invites, list)
assert isinstance(invites[0], PendingStreamCollaborator)
assert len(invites) == 1
invite = second_client.user.get_pending_invite(stream_id=stream.id)
assert isinstance(invite, PendingStreamCollaborator)
def test_stream_invite_use(self, second_client: SpeckleClient, stream: Stream):
invite: PendingStreamCollaborator = (
second_client.user.get_all_pending_invites()[0]
)
accepted = second_client.stream.invite_use(
stream_id=stream.id, token=invite.token
)
assert accepted is True
def test_stream_update_permission(
self, client: SpeckleClient, stream: Stream, second_user: User
):
updated = client.stream.update_permission(
stream_id=stream.id, user_id=second_user.id, role="stream:contributor"
)
assert updated is True
def test_stream_revoke_permission(self, client, stream, second_user):
revoked = client.stream.revoke_permission(
stream_id=stream.id, user_id=second_user.id
)
fetched_stream = client.stream.get(stream.id)
assert revoked is True
assert len(fetched_stream.collaborators) == 1
def test_stream_invite_cancel(
self,
client: SpeckleClient,
stream: Stream,
second_user: User,
):
invited = client.stream.invite(
stream_id=stream.id,
user_id=second_user.id,
message="welcome to my stream!",
)
assert invited is True
invites = client.stream.get_all_pending_invites(stream_id=stream.id)
cancelled = client.stream.invite_cancel(
invite_id=invites[0].inviteId, stream_id=stream.id
)
assert cancelled is True
def test_stream_invite_batch(
self, client: SpeckleClient, stream: Stream, second_user: User
):
# NOTE: only works for server admins
# invited = client.stream.invite_batch(
# stream_id=stream.id,
# emails=["userA@speckle.xyz", "userB@speckle.xyz"],
# user_ids=[second_user.id],
# message="yeehaw 🤠",
# )
# assert invited is True
# invited_only_email = client.stream.invite_batch(
# stream_id=stream.id,
# emails=["userC@speckle.xyz"],
# message="yeehaw 🤠",
# )
# assert invited_only_email is True
# fail if no emails or user ids
with pytest.raises(SpeckleException):
client.stream.invite_batch(stream_id=stream.id)
def test_stream_activity(self, client: SpeckleClient, stream: Stream):
activity = client.stream.activity(stream.id)
older_activity = client.stream.activity(
stream.id, before=activity.items[0].time
)
assert isinstance(activity, ActivityCollection)
assert isinstance(older_activity, ActivityCollection)
assert older_activity.totalCount < activity.totalCount
assert activity.items is not None
assert isinstance(activity.items[0], Activity)
def test_stream_delete(self, client, stream):
deleted = client.stream.delete(stream.id)
stream_get = client.stream.get(stream.id)
assert deleted is True
assert isinstance(stream_get, GraphQLException)

Some files were not shown because too many files have changed in this diff Show More