Compare commits

...

128 Commits

Author SHA1 Message Date
Gergő Jedlicska e6b822b0e3 Merge pull request #338 from specklesystems/gergo/automateExitCode
fix(automate): make sure we exit with code 0 if execution completes
2024-06-03 16:08:28 +02:00
Gergő Jedlicska 239bc4b5b9 docs(automate): finish comment block thoughts 2024-06-03 14:29:29 +02:00
Gergő Jedlicska 4eea15ddc1 fix(automate): make sure we exit with code 0 if execution completes 2024-06-03 14:27:07 +02:00
Aleksei Protopopov 204aa7466e Feature: adds connection_timeout argument to SpeckleClient (#337)
* Add connection_timeout argument to SpeckleClient

* Reformat code with black

* Set default timeout to 10s

* Make connection retries configurable
2024-05-27 14:23:39 +01:00
Gergő Jedlicska 24019e99f3 Merge pull request #335 from specklesystems/gergo/automateInterfaceRework
Rework automate SDK for the integrated automate api
2024-05-16 18:14:47 +02:00
Gergő Jedlicska 64492fafa5 fix: proper pytest skip 2024-05-16 17:24:53 +02:00
Gergő Jedlicska 3a8d634989 test: disable automation tests for now 2024-05-16 17:18:57 +02:00
Gergő Jedlicska f27650af3a feat: update automation schema and automation context for the new automate interfaces 2024-05-16 10:25:58 +02:00
KatKatKateryna 6469b6f757 Merge pull request #334 from specklesystems/jsdb/doc-strings-patch
Corrects and enhances user API class documentation (CNX-9172)
2024-03-28 23:42:09 +08:00
KatKatKateryna b28db0881c formatting 2024-03-28 16:22:21 +01:00
Jonathon Broughton b0b442de23 fix poetry in dockerfile 2024-03-28 14:48:34 +00:00
Jonathon Broughton 32d2fe8ead Merge branch 'main' into jsdb/doc-strings-patch 2024-03-26 12:48:45 +00:00
Jonathon Broughton 9fd40eac23 Update other_user.py 2024-03-26 12:43:14 +00:00
Benjamin Ottensten b22ba1f1f1 Update web app link in the README (#333) 2024-03-26 12:39:59 +00:00
Jonathon Broughton 5e20fe7bf1 Corrected/updated docstrings for method signatures 2024-03-26 11:06:35 +00:00
Jedd Morgan 6da5da23c4 feat(core): [CNX-9108] Added server migration support (#331)
* Added server migration support

* fix obvious mistake

* Fixed slightly less obvious mistake

* Run black

* isort
2024-03-25 11:15:41 +00:00
Gergő Jedlicska 1b59f0b026 feat: fix authenticate with token mechanism (#330) 2024-02-26 16:30:28 +00:00
Jedd Morgan 78123936d2 Merge pull request #329 from specklesystems/jrm/spirals-fix
fix(objects): [CNX-9014] Fixed issue with Spiral turns not deserializing in SpecklePy
2024-02-19 13:05:38 +00:00
Jedd Morgan dbc1aefed3 Fixed issue with Spiral turns not deserializing in SpecklePy 2024-02-19 12:41:35 +00:00
Gergő Jedlicska e726345b0c Merge pull request #328 from mortenengen/fix-gis-dict-type
fix: dict type of renderer in Layer
2024-02-13 08:30:48 +01:00
Morten Engen e074dbcced gis/layers: fix dict type of renderer in Layer 2024-02-12 20:54:38 +01:00
Gergő Jedlicska 62e342b2cb Merge pull request #302 from specklesystems/gergo/objects_init
fix: object initialization
2024-02-12 17:38:04 +01:00
Gergő Jedlicska 804dd37639 Merge branch 'main' into gergo/objects_init 2024-02-12 17:30:03 +01:00
Gergő Jedlicska 64b61f54f5 Merge pull request #322 from specklesystems/kate-2.17-hotfix
.url attribute was used before assignment; assign account to unauthen…
2024-02-12 17:29:50 +01:00
KatKatKateryna 58789ab234 undo 2024-02-12 15:33:21 +00:00
KatKatKateryna 2696fb74ba raise instream of returning Exception 2024-02-12 15:30:00 +00:00
KatKatKateryna 57e176af91 typo 2024-02-12 15:23:37 +00:00
KatKatKateryna 437483641c extra test 2024-02-12 15:23:01 +00:00
KatKatKateryna 1e971b57c3 formatting 2024-02-12 15:14:10 +00:00
KatKatKateryna f04be12ec8 formatting 2024-02-12 15:11:11 +00:00
KatKatKateryna 51242928ca remove circular import 2024-02-12 14:56:47 +00:00
KatKatKateryna 77b3be9145 formatting 2024-02-12 14:52:31 +00:00
KatKatKateryna cc5abdf9cb Merge branch 'main' into kate-2.17-hotfix 2024-02-12 13:52:34 +00:00
KatKatKateryna 4eca5144a8 unused import 2024-02-12 13:52:29 +00:00
KatKatKateryna 8589663049 don't get default account 2024-02-12 13:47:24 +00:00
KatKatKateryna 956f72dd6a formatting 2024-02-12 13:09:09 +00:00
KatKatKateryna a2daa68c1c Merge branch 'main' into gergo/objects_init 2024-02-12 13:00:00 +00:00
Gergő Jedlicska d60feb73a2 Merge pull request #259 from specklesystems/kate/branch_create_fix
gql minimum characters restriction for consistent behavior with frontend
2024-02-12 13:42:32 +01:00
KatKatKateryna a0ca10ad20 add to_string; add cases for object url in fe2 (#327)
* add to_string; add cases for object url in fe2

* cover exceptions

* add federated model exception, reorder conditions

* formatting

* reformatting

* update black formatter

* resolving dependencies
2024-02-09 18:35:20 +01:00
Gergő Jedlicska f6118f3336 Merge pull request #326 from specklesystems/isFrontend2
add frontend2 property to ServerInfo
2024-02-05 13:25:33 +01:00
KatKatKateryna c7cd2f3e91 test 2024-02-05 11:23:22 +00:00
KatKatKateryna b374bfefd0 reorder import 2024-02-05 11:17:51 +00:00
KatKatKateryna d716db251f add frontend2 property to ServerInfo 2024-02-05 10:40:13 +00:00
Gergő Jedlicska 6d7e7c5c4b Merge pull request #324 from specklesystems/gergo/expose_ssl_verification
Update client.py
2023-12-15 16:15:45 +01:00
Gergő Jedlicska 7dcd9288ca Merge branch 'main' into gergo/expose_ssl_verification 2023-12-15 11:19:09 +01:00
Jedd Morgan 7d99f48def Merge pull request #323 from specklesystems/gergo/init_subclass_fix
fix: CNX-8350 remove unnecessary kwargs from init subclass call chain
2023-12-14 14:46:40 +00:00
Jedd Morgan 4332a8faef Merge branch 'main' into gergo/init_subclass_fix 2023-12-14 14:44:13 +00:00
Gergő Jedlicska a1aee8b3fa Merge pull request #325 from specklesystems/gergo/file_based_automate_function_inputs
feat: read automation function inputs from file
2023-12-13 15:16:59 +01:00
Gergő Jedlicska deb8ad50c5 fix: client certificate verification 2023-12-11 17:34:26 +01:00
Gergő Jedlicska 558b25b1d1 feat: read automation function inputs from file 2023-12-11 17:30:08 +01:00
Gergő Jedlicska 4db0fa69fa Update client.py 2023-12-11 17:15:07 +01:00
Gergő Jedlicska 1eca211c96 fix: remove debug print statement 2023-12-06 11:50:07 +01:00
Gergő Jedlicska f65173581a fix: pre-commit config 2023-12-05 16:07:23 +01:00
Gergő Jedlicska 223c776c63 fix: remove unnecessary kwargs from init subclass call chain 2023-12-05 15:03:25 +01:00
KatKatKateryna ccccc53f59 .url attribute was used before assignment; assign account to unauthenticated client to get token 2023-12-05 05:21:28 +08:00
Gergő Jedlicska ae6fc85ab4 Merge pull request #320 from specklesystems/iain/configurable-certificate-verification
feat(client): configurable certificate verification
2023-11-27 14:36:28 +01:00
Gergő Jedlicska 7ad0785c62 chore: reformat 2023-11-27 14:33:31 +01:00
Gergő Jedlicska 76e4ec1535 Merge branch 'main' into iain/configurable-certificate-verification 2023-11-27 14:30:42 +01:00
Gergő Jedlicska 4e96aade51 Merge pull request #321 from specklesystems/iain/circleci-pre-commit
ci(pre-commit): add a pre-commit job to CI
2023-11-27 14:30:24 +01:00
Gergő Jedlicska 7ca00b7b77 chore: fix unused import 2023-11-27 14:27:09 +01:00
Gergő Jedlicska bddf9c0c1c chore: fix imports 2023-11-27 14:24:45 +01:00
Gergő Jedlicska bf3ab7da4c chore: reorder imports 2023-11-27 12:22:33 +01:00
Gergő Jedlicska 4dc148181e chore: reformat 2023-11-27 12:21:26 +01:00
Iain Sproat 357859288d chore(pre-commit autoupdate): install latest packages to prevent build errors 2023-11-24 16:41:47 +00:00
Iain Sproat 1ce61bdda8 ci(pre-commit): add a pre-commit job to CI 2023-11-24 16:37:51 +00:00
Iain Sproat 42737c4ed2 feat(client): configurable certificate verification 2023-11-24 15:52:30 +00:00
Gergő Jedlicska 62ee1a4b0a Merge pull request #319 from specklesystems/gergo/automate_report_logging
Gergo/automate report logging
2023-11-14 16:26:18 +01:00
Gergő Jedlicska d21373873c Merge branch 'main' into gergo/automate_report_logging 2023-11-14 16:23:25 +01:00
Gergő Jedlicska e3716f6206 feat: log reported run status 2023-11-14 16:23:10 +01:00
Gergő Jedlicska f6917b0761 Merge pull request #318 from specklesystems/gergo/markLogs
feat: reduce log message context for object results
2023-11-13 11:39:49 +01:00
Gergő Jedlicska 04764b17eb feat: reduce log message context for object results 2023-11-13 09:14:06 +01:00
Gergő Jedlicska dbe3d759f6 Merge pull request #317 from specklesystems/KatKatKateryna-patch-1
handle "append" with incoming type "list"
2023-11-13 09:09:23 +01:00
KatKatKateryna f6ff484e66 handle "append" with incoming type "list" 2023-11-13 04:53:03 +08:00
Gergő Jedlicska bd000395af Merge pull request #316 from specklesystems/gergo/contextViewFix
fix: report relative url for context view
2023-11-11 08:32:40 +01:00
Gergő Jedlicska 10f49579fd fix: report relative url for context view 2023-11-11 08:29:16 +01:00
Gergő Jedlicska 1693465dfc Merge pull request #314 from specklesystems/kate/stream_wrapper_fe2
update stream wrapper; add tests
2023-11-10 15:15:12 +01:00
Gergő Jedlicska c3a7ead8f5 Merge branch 'main' into kate/stream_wrapper_fe2 2023-11-10 15:11:28 +01:00
Gergő Jedlicska d151a8d0ae Merge pull request #315 from specklesystems/jrm/minio/fix
Pinned MinIO version for integration test docker compose
2023-11-10 15:06:14 +01:00
Gergő Jedlicska c0dd88cbdb fix: pin minio release to fix tests 2023-11-10 15:04:30 +01:00
Jedd Morgan 71d3589e72 pinned MinIO version for integration test docker compose 2023-11-10 13:54:02 +00:00
KatKatKateryna 5bde1bc2d6 remove library 2023-11-09 17:36:06 +00:00
KatKatKateryna 75e6f0229a update stream wrapper; add tests 2023-11-09 17:34:57 +00:00
Gergő Jedlicska 5d7e71f357 Merge pull request #313 from specklesystems/oguzhan/text-object
Chore (Objects): Add text object definition
2023-10-30 12:37:02 +01:00
oguzhankoral 6c223b6fb3 Exclude displayStyle from Text object
It shouldn't be have displayStyle for general purpose Text object, because displayStyle more Rhino and AutoCAD specific
2023-10-30 14:07:36 +03:00
oguzhankoral e6131a7956 Fix typo on type of displayValue 2023-10-30 11:51:15 +03:00
oguzhankoral 45b50e4f26 Add optional props of Objects.Other.Text 2023-10-30 11:42:30 +03:00
oguzhankoral d9b92490ec Add text object definition 2023-10-27 16:33:02 +03:00
Gergő Jedlicska 37c09fa56c Merge pull request #311 from specklesystems/gergo/contextView
fix: automate sdk context view is a realative url
2023-10-26 15:38:50 +02:00
Gergő Jedlicska cbae4d300d Merge branch 'main' into gergo/contextView 2023-10-26 15:36:30 +02:00
Gergő Jedlicska 2742c12e31 fix: automate sdk context view is a realative url 2023-10-26 15:35:39 +02:00
Gergő Jedlicska 6dd0813089 Merge pull request #310 from specklesystems/gergo/contextView
Gergo/context view
2023-10-26 15:02:37 +02:00
Gergő Jedlicska a1831b57db feat: automation context result view reporting and creating 2023-10-26 15:00:03 +02:00
Gergő Jedlicska 1ff3245531 feat: automate sdk report context view 2023-10-26 13:32:48 +02:00
Gergő Jedlicska 3b4723a186 Merge pull request #309 from specklesystems/gergo/automateReportFunctionName
feat: migrate to new automate api
2023-10-25 18:06:44 +02:00
Gergő Jedlicska efe9551c5e fix: legacy typing and tests 2023-10-25 17:53:23 +02:00
Gergő Jedlicska 23a5087fbc feat: migrate to new automate api 2023-10-25 17:46:00 +02:00
Gergő Jedlicska 52c8e37a5b Merge pull request #305 from specklesystems/gergo/automation_runner_refactor
gergo/automation runner refactor
2023-10-11 11:10:41 +02:00
Gergő Jedlicska 6a6b3d4c3d ci: disable docker layer caching 2023-10-11 11:06:30 +02:00
Gergő Jedlicska 8f32aa014e ci: release the pin 2023-10-11 11:03:08 +02:00
Gergő Jedlicska 11c6221972 ci: pin to new server image 2023-10-11 10:59:12 +02:00
Gergő Jedlicska 262be44423 chore: bump package version 2023-10-11 09:54:44 +02:00
Gergő Jedlicska fd3d97cf5a Merge branch 'main' into gergo/automation_runner_refactor 2023-10-10 18:09:40 +02:00
Gergő Jedlicska 9dba99ad26 Merge pull request #308 from specklesystems/gergo/spiralTurnsFix
fix(objects): spiral turns should be optional floats
2023-10-09 15:13:21 +02:00
Gergő Jedlicska 2810598336 fix(objects): spiral turns should be optional floats 2023-10-09 14:25:27 +02:00
Gergő Jedlicska f918582ed2 Merge pull request #307 from specklesystems/branch_id_name
pass branch name to commit.create
2023-10-04 15:53:41 +02:00
KatKatKateryna 9181440c62 pass branch name to commit.create 2023-10-04 14:24:30 +02:00
Gergő Jedlicska 62912d4428 Merge pull request #306 from specklesystems/gergo/automateRaise
fix(automate_sdk): make sure we throw for failed version create
2023-10-03 16:08:00 +02:00
Gergő Jedlicska 67cf41d721 fix(automate_sdk): get model name from id 2023-10-03 16:04:22 +02:00
Gergő Jedlicska 4ad3761478 fix(automate_sdk): make sure we throw for failed version create 2023-10-03 15:28:50 +02:00
Gergő Jedlicska 6e8e08ae94 fix(automate): support py >= 3.10 typing 2023-10-03 08:13:40 +02:00
Gergő Jedlicska 6e7c36223f fix(automate_sdk): functions have releases 2023-10-02 14:03:07 +02:00
Gergő Jedlicska b1f979a10a WIP: rework result schema 2023-10-02 14:00:34 +02:00
Gergő Jedlicska d1ebd84cca extract useful functions to helpers module 2023-09-21 19:02:42 +02:00
Gergő Jedlicska fe92e49c59 refactor run automation to directly take in a context 2023-09-21 15:24:04 +02:00
Gergő Jedlicska fbbd6c0dd7 Merge pull request #303 from specklesystems/gergo/speckle_automate
feat: add speckle automate package with some basic sanity tests
2023-09-20 10:06:10 +02:00
Gergő Jedlicska 8ffe219111 chore: update deps 2023-09-19 20:22:57 +02:00
Gergő Jedlicska e4d087db3a fix: we still support py38 2023-09-19 20:21:46 +02:00
Gergő Jedlicska 2e8943e961 feat: test against latest 2023-09-19 20:18:47 +02:00
Gergő Jedlicska f254defc6b feat: add speckle automate package with some basic sanity tests 2023-09-19 20:11:32 +02:00
KatKatKateryna 541e3d961f moving restrictions to core 2023-09-19 10:56:39 +01:00
KatKatKateryna b02f183533 Merge branch 'main' into kate/branch_create_fix 2023-09-19 10:52:12 +01:00
Gergő Jedlicska 589198f5f1 fix: object initialization
now every time specklepy is imported, all object definitions are initialized.
 This ensures, that all speckle types are registered.
2023-09-19 11:31:57 +02:00
KatKatKateryna 948a56a07f Merge pull request #300 from specklesystems/2.16_backwards-compatibility
add deprecated gis classes
2023-09-18 13:07:19 +01:00
KatKatKateryna 3eed9a60fa add deprecated gis classes 2023-09-08 19:28:14 +01:00
Jedd Morgan c169c4eeda Merge pull request #299 from specklesystems/jrm/units/units-brep-fix
fix(objects)): Fixed issue where breps were incorrectly setting their…
2023-09-08 13:45:46 +01:00
Jedd Morgan 32b5ef88a1 fix(tests): fixed unit test for brep serialization 2023-09-08 13:43:50 +01:00
Jedd Morgan 3a979318ad fix(objects)): Fixed issue where breps were incorrectly setting their units 2023-09-07 17:33:01 +01:00
KatKatKateryna 6a9f4bf89b gql minimum characters restriction for consistent behavior with frontend 2023-02-08 07:29:02 +08:00
79 changed files with 3026 additions and 1448 deletions
+41 -3
View File
@@ -1,14 +1,47 @@
version: 2.1
orbs:
python: circleci/python@2.0.3
codecov: codecov/codecov@3.2.2
codecov: codecov/codecov@3.3.0
jobs:
pre-commit:
parameters:
config_file:
default: ./.pre-commit-config.yaml
description: Optional, path to pre-commit config file.
type: string
cache_prefix:
default: ''
description: |
Optional cache prefix to be used on CircleCI. Can be used for cache busting or to ensure multiple jobs use different caches.
type: string
docker:
- image: speckle/pre-commit-runner:latest
resource_class: medium
steps:
- checkout
- restore_cache:
keys:
- cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
- run:
name: Install pre-commit hooks
command: pre-commit install-hooks --config <<parameters.config_file>>
- save_cache:
key: cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
paths:
- ~/.cache/pre-commit
- run:
name: Run pre-commit
command: pre-commit run --all-files
- run:
command: git --no-pager diff
name: git diff
when: on_fail
test:
machine:
image: ubuntu-2204:2023.02.1
docker_layer_caching: true
docker_layer_caching: false
resource_class: medium
parameters:
tag:
@@ -52,6 +85,10 @@ jobs:
workflows:
main:
jobs:
- pre-commit:
filters:
tags:
only: /.*/
- test:
matrix:
parameters:
@@ -62,6 +99,7 @@ workflows:
- deploy:
context: pypi
requires:
- pre-commit
- test
filters:
tags:
+1 -1
View File
@@ -22,6 +22,6 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
USER vscode
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH=$PATH:$HOME/.poetry/env
+9 -9
View File
@@ -2,23 +2,23 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
hooks:
- id: ruff
rev: v0.0.186
rev: v0.1.6
- repo: https://github.com/commitizen-tools/commitizen
hooks:
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: v2.38.0
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: v3.13.0
- repo: https://github.com/pycqa/isort
rev: v5.11.3
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 23.11.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
@@ -27,7 +27,7 @@ repos:
# 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
rev: v4.5.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+1 -1
View File
@@ -33,7 +33,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
- [![speckle](https://img.shields.io/badge/https://-app.speckle.systems-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
+6 -6
View File
@@ -33,7 +33,7 @@ services:
retries: 30
minio:
image: "minio/minio"
image: "minio/minio:RELEASE.2023-10-25T06-33-25Z"
command: server /data --console-address ":9001"
restart: always
volumes:
@@ -53,7 +53,7 @@ services:
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend:2
image: speckle/speckle-frontend:latest
restart: always
ports:
- "0.0.0.0:8080:8080"
@@ -61,7 +61,7 @@ services:
FILE_SIZE_LIMIT_MB: 100
speckle-server:
image: speckle/speckle-server:2
image: speckle/speckle-server:latest
restart: always
healthcheck:
test:
@@ -112,7 +112,7 @@ services:
ENABLE_MP: "false"
preview-service:
image: speckle/speckle-preview-service:2
image: speckle/speckle-preview-service:latest
restart: always
depends_on:
speckle-server:
@@ -124,7 +124,7 @@ services:
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle"
webhook-service:
image: speckle/speckle-webhook-service:2
image: speckle/speckle-webhook-service:latest
restart: always
depends_on:
speckle-server:
@@ -135,7 +135,7 @@ services:
WAIT_HOSTS: postgres:5432
fileimport-service:
image: speckle/speckle-fileimport-service:2
image: speckle/speckle-fileimport-service:latest
restart: always
depends_on:
speckle-server:
Generated
+936 -937
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.9.1"
version = "2.17.14"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -10,21 +10,23 @@ documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
packages = [
{ include = "specklepy", from = "src" },
{ include = "speckle_automate", from = "src" },
]
[tool.poetry.dependencies]
python = ">=3.7.2, <4.0"
pydantic = "^2.0"
python = ">=3.8.0, <4.0"
pydantic = "^2.5"
appdirs = "^1.4.4"
gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
gql = { extras = ["requests", "websockets"], version = "^3.3.0" }
ujson = "^5.3.0"
Deprecated = "^1.2.13"
stringcase = "^1.2.0"
attrs = "^23.1.0"
httpx = "^0.25.0"
[tool.poetry.group.dev.dependencies]
black = "^22.8.0"
black = "23.11.0"
isort = "^5.7.0"
pytest = "^7.1.3"
pytest-ordering = "^0.6"
@@ -34,7 +36,7 @@ pylint = "^2.14.4"
mypy = "^0.982"
pre-commit = "^2.20.0"
commitizen = "^2.38.0"
ruff = "^0.0.187"
ruff = "^0.4.4"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
+23
View File
@@ -0,0 +1,23 @@
"""This module contains an SDK for working with Speckle Automate."""
from speckle_automate.automation_context import AutomationContext
from speckle_automate.runner import execute_automate_function, run_function
from speckle_automate.schema import (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResultLevel,
ResultCase,
)
__all__ = [
"AutomationContext",
"AutomateBase",
"AutomationStatus",
"AutomationResult",
"AutomationRunData",
"ResultCase",
"ObjectResultLevel",
"run_function",
"execute_automate_function",
]
+402
View File
@@ -0,0 +1,402 @@
"""This module provides an abstraction layer above the Speckle Automate runtime."""
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
import httpx
from gql import gql
from speckle_automate.schema import (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResultLevel,
ResultCase,
)
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api.models import Branch
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.transports.memory import MemoryTransport
from specklepy.transports.server import ServerTransport
@dataclass
class AutomationContext:
"""A context helper class.
This class exposes methods to work with the Speckle Automate context inside
Speckle Automate functions.
An instance of AutomationContext is injected into every run of a function.
"""
automation_run_data: AutomationRunData
speckle_client: SpeckleClient
_server_transport: ServerTransport
_speckle_token: str
#: keep a memory transponrt at hand, to speed up things if needed
_memory_transport: MemoryTransport = field(default_factory=MemoryTransport)
#: added for performance measuring
_init_time: float = field(default_factory=time.perf_counter)
_automation_result: AutomationResult = field(default_factory=AutomationResult)
@classmethod
def initialize(
cls, automation_run_data: Union[str, AutomationRunData], speckle_token: str
) -> "AutomationContext":
"""Bootstrap the AutomateSDK from raw data.
Todo:
----
* bootstrap a structlog logger instance
* expose a logger, that ppl can use instead of print
"""
# parse the json value if its not an initialized project data instance
automation_run_data = (
automation_run_data
if isinstance(automation_run_data, AutomationRunData)
else AutomationRunData.model_validate_json(automation_run_data)
)
speckle_client = SpeckleClient(
automation_run_data.speckle_server_url,
automation_run_data.speckle_server_url.startswith("https"),
)
speckle_client.authenticate_with_token(speckle_token)
if not speckle_client.account:
msg = (
f"Could not autenticate to {automation_run_data.speckle_server_url}",
"with the provided token",
)
raise ValueError(msg)
server_transport = ServerTransport(
automation_run_data.project_id, speckle_client
)
return cls(automation_run_data, speckle_client, server_transport, speckle_token)
@property
def run_status(self) -> AutomationStatus:
"""Get the status of the automation run."""
return self._automation_result.run_status
@property
def status_message(self) -> Optional[str]:
"""Get the current status message."""
return self._automation_result.status_message
def elapsed(self) -> float:
"""Return the elapsed time in seconds since the initialization time."""
return time.perf_counter() - self._init_time
def receive_version(self) -> Base:
"""Receive the Speckle project version that triggered this automation run."""
# TODO: this is a quick hack to keep implementation consistency. Move to proper receive many versions
version_id = self.automation_run_data.triggers[0].payload.version_id
commit = self.speckle_client.commit.get(
self.automation_run_data.project_id, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
base = operations.receive(
commit.referencedObject, self._server_transport, self._memory_transport
)
print(
f"It took {self.elapsed():.2f} seconds to receive",
f" the speckle version {version_id}",
)
return base
def create_new_version_in_project(
self, root_object: Base, model_name: str, version_message: str = ""
) -> Tuple[str, str]:
"""Save a base model to a new version on the project.
Args:
root_object (Base): The Speckle base object for the new version.
model_id (str): For now please use a `branchName`!
version_message (str): The message for the new version.
"""
branch = self.speckle_client.branch.get(
self.automation_run_data.project_id, model_name, 1
)
if isinstance(branch, Branch):
if not branch.id:
raise ValueError("Cannot use the branch without its id")
matching_trigger = [
t
for t in self.automation_run_data.triggers
if t.payload.model_id == branch.id
]
if matching_trigger:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {matching_trigger[0].payload.model_id}"
)
model_id = branch.id
else:
# we just check if it exists
branch_create = self.speckle_client.branch.create(
self.automation_run_data.project_id,
model_name,
)
if isinstance(branch_create, Exception):
raise branch_create
model_id = branch_create
root_object_id = operations.send(
root_object,
[self._server_transport, self._memory_transport],
use_default_cache=False,
)
version_id = self.speckle_client.commit.create(
stream_id=self.automation_run_data.project_id,
object_id=root_object_id,
branch_name=model_name,
message=version_message,
source_application="SpeckleAutomate",
)
if isinstance(version_id, SpeckleException):
raise version_id
self._automation_result.result_versions.append(version_id)
return model_id, version_id
@property
def context_view(self) -> Optional[str]:
return self._automation_result.result_view
def set_context_view(
self,
# f"{model_id}@{version_id} or {model_id} "
resource_ids: Optional[List[str]] = None,
include_source_model_version: bool = True,
) -> None:
link_resources = (
[
f"{t.payload.model_id}@{t.payload.version_id}"
for t in self.automation_run_data.triggers
]
if include_source_model_version
else []
)
if resource_ids:
link_resources.extend(resource_ids)
if not link_resources:
raise Exception(
"We do not have enough resource ids to compose a context view"
)
self._automation_result.result_view = (
f"/projects/{self.automation_run_data.project_id}"
f"/models/{','.join(link_resources)}"
)
def report_run_status(self) -> None:
"""Report the current run status to the project of this automation."""
query = gql(
"""
mutation AutomateFunctionRunStatusReport(
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
$results: JSONObject
$contextView: String
){
automateFunctionRunStatusReport(input: {
functionRunId: $functionRunId
status: $status
statusMessage: $statusMessage
contextView: $contextView
results: $results
})
}
"""
)
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
object_results = {
"version": 1,
"values": {
"objectResults": self._automation_result.model_dump(by_alias=True)[
"objectResults"
],
"blobIds": self._automation_result.blobs,
},
}
else:
object_results = None
params = {
"functionRunId": self.automation_run_data.function_run_id,
"status": self.run_status.value,
"statusMessage": self._automation_result.status_message,
"results": object_results,
"contextView": self._automation_result.result_view,
}
print(f"Reporting run status with content: {params}")
self.speckle_client.httpclient.execute(query, params)
def store_file_result(self, file_path: Union[Path, str]) -> None:
"""Save a file attached to the project of this automation."""
path_obj = (
Path(file_path).resolve() if isinstance(file_path, str) else file_path
)
class UploadResult(AutomateBase):
blob_id: str
file_name: str
upload_status: int
class BlobUploadResponse(AutomateBase):
upload_results: list[UploadResult]
if not path_obj.exists():
raise ValueError("The given file path doesn't exist")
files = {path_obj.name: open(str(path_obj), "rb")}
url = (
f"{self.automation_run_data.speckle_server_url}/api/stream/"
f"{self.automation_run_data.project_id}/blob"
)
data = (
httpx.post(
url,
files=files,
headers={"authorization": f"Bearer {self._speckle_token}"},
)
.raise_for_status()
.json()
)
upload_response = BlobUploadResponse.model_validate(data)
if len(upload_response.upload_results) != 1:
raise ValueError("Expecting one upload result.")
self._automation_result.blobs.extend(
[upload_result.blob_id for upload_result in upload_response.upload_results]
)
def mark_run_failed(self, status_message: str) -> None:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.FAILED, status_message)
def mark_run_success(self, status_message: Optional[str]) -> None:
"""Mark the current run a success with an optional message."""
self._mark_run(AutomationStatus.SUCCEEDED, status_message)
def _mark_run(
self, status: AutomationStatus, status_message: Optional[str]
) -> None:
duration = self.elapsed()
self._automation_result.status_message = status_message
self._automation_result.run_status = status
self._automation_result.elapsed = duration
msg = f"Automation run {status.value} after {duration:.2f} seconds."
print("\n".join([msg, status_message]) if status_message else msg)
def attach_error_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new error case to the run results.
If the error cause has already created an error case,
the error will be extended with a new case refering to the causing objects.
Args:
error_tag (str): A short tag for the error type.
causing_object_ids (str[]): A list of object_id-s that are causing the error
error_messagge (Optional[str]): Optional error message.
metadata: User provided metadata key value pairs
visual_overrides: Case specific 3D visual overrides.
"""
self.attach_result_to_objects(
ObjectResultLevel.ERROR,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_warning_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new warning case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.WARNING,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_info_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new info case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.INFO,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_result_to_objects(
self,
level: ObjectResultLevel,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
if isinstance(object_ids, list):
if len(object_ids) < 1:
raise ValueError(
f"Need atleast one object_id to report a(n) {level.value.upper()}"
)
id_list = object_ids
else:
id_list = [object_ids]
print(
f"Created new {level.value.upper()}"
f" category: {category} caused by: {message}"
)
self._automation_result.object_results.append(
ResultCase(
category=category,
level=level,
object_ids=id_list,
message=message,
metadata=metadata,
visual_overrides=visual_overrides,
)
)
+55
View File
@@ -0,0 +1,55 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
from gql import gql
from specklepy.api.client import SpeckleClient
def register_new_automation(
speckle_client: SpeckleClient,
project_id: str,
model_id: str,
automation_id: str,
automation_name: str,
automation_revision_id: str,
) -> bool:
"""Register a new automation in the speckle server."""
query = gql(
"""
mutation CreateAutomation(
$projectId: String!
$modelId: String!
$automationName: String!
$automationId: String!
$automationRevisionId: String!
) {
automationMutations {
create(
input: {
projectId: $projectId
modelId: $modelId
automationName: $automationName
automationId: $automationId
automationRevisionId: $automationRevisionId
}
)
}
}
"""
)
params = {
"projectId": project_id,
"modelId": model_id,
"automationName": automation_name,
"automationId": automation_id,
"automationRevisionId": automation_revision_id,
}
return speckle_client.httpclient.execute(query, params)
def crypto_random_string(length: int) -> str:
"""Generate a semi crypto random string of a given length."""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length)).lower()
+193
View File
@@ -0,0 +1,193 @@
"""Function execution module.
Provides mechanisms to execute any function,
that conforms to the AutomateFunction "interface"
"""
import json
import sys
import traceback
from pathlib import Path
from typing import Callable, Optional, Tuple, TypeVar, Union, overload
from pydantic import create_model
from pydantic.json_schema import GenerateJsonSchema
from speckle_automate.automation_context import AutomationContext
from speckle_automate.schema import AutomateBase, AutomationRunData, AutomationStatus
T = TypeVar("T", bound=AutomateBase)
AutomateFunction = Callable[[AutomationContext, T], None]
AutomateFunctionWithoutInputs = Callable[[AutomationContext], None]
def _read_input_data(inputs_location: str) -> str:
input_path = Path(inputs_location)
if not input_path.exists():
raise ValueError(f"Cannot find the function inputs file at {input_path}")
return input_path.read_text()
def _parse_input_data(
input_location: str, input_schema: Optional[type[T]]
) -> Tuple[AutomationRunData, Optional[T], str]:
input_json_string = _read_input_data(input_location)
class FunctionRunData(AutomateBase):
speckle_token: str
automation_run_data: AutomationRunData
function_inputs: None = None
parser_model = FunctionRunData
if input_schema:
parser_model = create_model(
"FunctionRunDataWithInputs",
function_inputs=(input_schema, ...),
__base__=FunctionRunData,
)
input_data = parser_model.model_validate_json(input_json_string)
return (
input_data.automation_run_data,
input_data.function_inputs,
input_data.speckle_token,
)
@overload
def execute_automate_function(
automate_function: AutomateFunction[T],
input_schema: type[T],
) -> None:
...
@overload
def execute_automate_function(
automate_function: AutomateFunctionWithoutInputs,
) -> None:
...
class AutomateGenerateJsonSchema(GenerateJsonSchema):
def generate(self, schema, mode="validation"):
json_schema = super().generate(schema, mode=mode)
json_schema["$schema"] = self.schema_dialect
return json_schema
def execute_automate_function(
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
input_schema: Optional[type[T]] = None,
):
"""Runs the provided automate function with the input schema."""
# first arg is the python file name, we do not need that
args = sys.argv[1:]
if len(args) != 2:
raise ValueError("Incorrect number of arguments specified need 2")
# we rely on a command name convention to decide what to do.
# this is here, so that the function authors do not see any of this
command, argument = args
if command == "generate_schema":
path = Path(argument)
schema = json.dumps(
input_schema.model_json_schema(
by_alias=True, schema_generator=AutomateGenerateJsonSchema
)
if input_schema
else {}
)
path.write_text(schema)
elif command == "run":
automation_run_data, function_inputs, speckle_token = _parse_input_data(
argument, input_schema
)
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
if function_inputs:
automation_context = run_function(
automation_context,
automate_function, # type: ignore
function_inputs, # type: ignore
)
else:
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
automation_context = run_function(
automation_context,
automate_function, # type: ignore
)
# if we've gotten this far, the execution should technically be completed as expected
# thus exiting with 0 is the schemantically correct thing to do
exit(0)
else:
raise NotImplementedError(f"Command: '{command}' is not supported.")
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunction[T],
inputs: T,
) -> AutomationContext:
...
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunctionWithoutInputs,
) -> AutomationContext:
...
def run_function(
automation_context: AutomationContext,
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
inputs: Optional[T] = None,
) -> AutomationContext:
"""Run the provided function with the automate sdk context."""
automation_context.report_run_status()
try:
# avoiding complex type gymnastics here on the internals.
# the external type overloads make this correct
if inputs:
automate_function(automation_context, inputs) # type: ignore
else:
automate_function(automation_context) # type: ignore
# the function author forgot to mark the function success
if automation_context.run_status not in [
AutomationStatus.FAILED,
AutomationStatus.SUCCEEDED,
]:
automation_context.mark_run_success(
"WARNING: Automate assumed a success status,"
" but it was not marked as so by the function."
)
except Exception:
trace = traceback.format_exc()
print(trace)
automation_context.mark_run_failed(
"Function error. Check the automation run logs for details."
)
finally:
if not automation_context.context_view:
automation_context.set_context_view()
automation_context.report_run_status()
return automation_context
+83
View File
@@ -0,0 +1,83 @@
""""""
from enum import Enum
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from stringcase import camelcase
class AutomateBase(BaseModel):
"""Use this class as a base model for automate related DTO."""
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
"""Represents the version creation trigger payload."""
model_id: str
version_id: str
class VersionCreationTrigger(AutomateBase):
"""Represents a single version creation trigger for the automation run."""
trigger_type: Literal["versionCreation"]
payload: VersionCreationTriggerPayload
class AutomationRunData(BaseModel):
"""Values of the project / model that triggered the run of this function."""
project_id: str
speckle_server_url: str
automation_id: str
automation_run_id: str
function_run_id: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
class AutomationStatus(str, Enum):
"""Set the status of the automation."""
INITIALIZING = "INITIALIZING"
RUNNING = "RUNNING"
FAILED = "FAILED"
SUCCEEDED = "SUCCEEDED"
class ObjectResultLevel(str, Enum):
"""Possible status message levels for object reports."""
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
class ResultCase(AutomateBase):
"""A result case."""
category: str
level: ObjectResultLevel
object_ids: List[str]
message: Optional[str]
metadata: Optional[Dict[str, Any]]
visual_overrides: Optional[Dict[str, Any]]
class AutomationResult(AutomateBase):
"""Schema accepted by the Speckle server as a result for an automation run."""
elapsed: float = 0
result_view: Optional[str] = None
result_versions: List[str] = Field(default_factory=list)
blobs: List[str] = Field(default_factory=list)
run_status: AutomationStatus = AutomationStatus.RUNNING
status_message: Optional[str] = None
object_results: list[ResultCase] = Field(default_factory=list)
+3
View File
@@ -0,0 +1,3 @@
from specklepy import objects
__all__ = ["objects"]
+19 -18
View File
@@ -1,17 +1,7 @@
import re
from typing import Dict
from warnings import warn
from deprecated import deprecated
from gql import Client
from gql.transport.exceptions import TransportServerError
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.credentials import Account
from specklepy.api.resources import (
user,
active_user,
branch,
commit,
@@ -20,11 +10,10 @@ from specklepy.api.resources import (
server,
stream,
subscriptions,
user,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
from specklepy.logging import metrics
class SpeckleClient(CoreSpeckleClient):
@@ -61,10 +50,16 @@ class SpeckleClient(CoreSpeckleClient):
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
) -> None:
super().__init__(
host=host,
use_ssl=use_ssl,
verify_certificate=verify_certificate,
)
self.account = Account()
@@ -131,7 +126,9 @@ class SpeckleClient(CoreSpeckleClient):
Arguments:
token {str} -- an api token
"""
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate_deprecated"})
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate_deprecated"}
)
return super().authenticate(token)
def authenticate_with_token(self, token: str) -> None:
@@ -143,7 +140,9 @@ class SpeckleClient(CoreSpeckleClient):
Arguments:
token {str} -- an api token
"""
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate With Token"})
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate With Token"}
)
return super().authenticate_with_token(token)
def authenticate_with_account(self, account: Account) -> None:
@@ -155,5 +154,7 @@ class SpeckleClient(CoreSpeckleClient):
account {Account} -- the account object which can be found with
`get_default_account` or `get_local_accounts`
"""
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate With Account"})
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate With Account"}
)
return super().authenticate_with_account(account)
+13 -17
View File
@@ -1,20 +1,14 @@
import os
from typing import List, Optional
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from specklepy.api.models import ServerInfo
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
# following imports seem to be unnecessary, but they need to stay
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.credentials import (Account, UserInfo,
StreamWrapper, # deprecated
get_local_accounts as core_get_local_accounts,
get_account_from_token as core_get_account_from_token)
from specklepy.core.api.credentials import StreamWrapper # noqa: F401
from specklepy.core.api.credentials import Account, UserInfo # noqa: F401
from specklepy.core.api.credentials import (
get_account_from_token as core_get_account_from_token,
)
from specklepy.core.api.credentials import get_local_accounts as core_get_local_accounts
from specklepy.logging import metrics
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
@@ -35,11 +29,12 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
(acc for acc in accounts if acc.isDefault),
accounts[0] if accounts else None,
),
{"name": "Get Local Accounts"}
{"name": "Get Local Accounts"},
)
return accounts
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
"""
Gets this environment's default account if any. If there is no default,
@@ -61,7 +56,8 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
metrics.initialise_tracker(default)
return default
def get_account_from_token(token: str, server_url: str = None) -> Account:
"""Gets the local account for the token if it exists
Arguments:
@@ -73,5 +69,5 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
"""
account = core_get_account_from_token(token, server_url)
metrics.track( metrics.SDK, account, {"name": "Get Account From Token"} )
metrics.track(metrics.SDK, account, {"name": "Get Account From Token"})
return account
+73 -17
View File
@@ -1,18 +1,74 @@
from dataclasses import dataclass
from enum import Enum
from unicodedata import name
from specklepy.core.api.host_applications import (
ARCGIS,
ARCHICAD,
AUTOCAD,
BLENDER,
CIVIL,
CSIBRIDGE,
DXF,
DYNAMO,
ETABS,
EXCEL,
GRASSHOPPER,
GSA,
MICROSTATION,
NET,
OPENBUILDINGS,
OPENRAIL,
OPENROADS,
OTHER,
POWERBI,
PYTHON,
QGIS,
REVIT,
RHINO,
SAFE,
SAP2000,
SKETCHUP,
TEKLASTRUCTURES,
TOPSOLID,
UNITY,
UNREAL,
HostApplication,
HostAppVersion,
_app_name_host_app_mapping,
get_host_app_from_string,
)
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.host_applications import (HostApplication, HostAppVersion,
get_host_app_from_string,
_app_name_host_app_mapping,
RHINO,GRASSHOPPER,REVIT,DYNAMO,UNITY,GSA,
CIVIL,AUTOCAD,MICROSTATION,OPENROADS,
OPENRAIL,OPENBUILDINGS,ETABS,SAP2000,CSIBRIDGE,
SAFE,TEKLASTRUCTURES,DXF,EXCEL,UNREAL,POWERBI,
BLENDER,QGIS,ARCGIS,SKETCHUP,ARCHICAD,TOPSOLID,
PYTHON,NET,OTHER)
if __name__ == "__main__":
print(HostAppVersion.v)
# re-exporting stuff from the moved api module
__all__ = [
"ARCGIS",
"ARCHICAD",
"AUTOCAD",
"BLENDER",
"CIVIL",
"CSIBRIDGE",
"DXF",
"DYNAMO",
"ETABS",
"EXCEL",
"GRASSHOPPER",
"GSA",
"MICROSTATION",
"NET",
"OPENBUILDINGS",
"OPENRAIL",
"OPENROADS",
"OTHER",
"POWERBI",
"PYTHON",
"QGIS",
"REVIT",
"RHINO",
"SAFE",
"SAP2000",
"SKETCHUP",
"TEKLASTRUCTURES",
"TOPSOLID",
"UNITY",
"UNREAL",
"HostApplication",
"HostAppVersion",
"_app_name_host_app_mapping",
"get_host_app_from_string",
]
+34 -11
View File
@@ -1,12 +1,35 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
# following imports seem to be unnecessary, but they need to stay
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.models import (Collaborator, Commit,
Commits, Object, Branch, Branches,
Stream, Streams, User, LimitedUser,
PendingStreamCollaborator, Activity,
ActivityCollection, ServerInfo)
from specklepy.core.api.models import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
LimitedUser,
Object,
PendingStreamCollaborator,
ServerInfo,
Stream,
Streams,
User,
)
__all__ = [
"Activity",
"ActivityCollection",
"Branch",
"Branches",
"Collaborator",
"Commit",
"Commits",
"LimitedUser",
"Object",
"PendingStreamCollaborator",
"ServerInfo",
"Stream",
"Streams",
"User",
]
+5 -8
View File
@@ -1,16 +1,12 @@
from typing import List, Optional
from specklepy.core.api.operations import deserialize as core_deserialize
from specklepy.core.api.operations import receive as _untracked_receive
from specklepy.core.api.operations import send as core_send
from specklepy.core.api.operations import serialize as core_serialize
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
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
from specklepy.core.api.operations import (send as core_send,
receive as _untracked_receive,
serialize as core_serialize,
deserialize as core_deserialize)
def send(
@@ -74,6 +70,7 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
metrics.track(metrics.SDK, custom_props={"name": "Serialize"})
return core_serialize(base, write_transports)
def deserialize(
obj_string: str, read_transport: Optional[AbstractTransport] = None
) -> Base:
+6 -20
View File
@@ -1,21 +1,8 @@
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from typing import Any, Optional, Tuple
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
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.resource import ResourceBase as CoreResourceBase
@@ -29,10 +16,9 @@ class ResourceBase(CoreResourceBase):
server_version: Optional[Tuple[Any, ...]] = None,
) -> None:
super().__init__(
account = account,
basepath = basepath,
client = client,
name = name,
server_version = server_version
account=account,
basepath=basepath,
client=client,
name=name,
server_version=server_version,
)
+32 -47
View File
@@ -1,18 +1,14 @@
from datetime import datetime, timezone
from datetime import datetime
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
from specklepy.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.active_user import Resource as CoreResource
from specklepy.logging import metrics
class Resource(CoreResource):
"""API Access class for users"""
"""API Access class for users. This class provides methods to get and update
the user profile, fetch user activity, and manage pending stream invitations."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -24,13 +20,9 @@ class Resource(CoreResource):
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
"""Gets the profile of the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
@@ -46,11 +38,11 @@ class Resource(CoreResource):
):
"""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
Args:
name (Optional[str]): The user's name.
company (Optional[str]): The company the user works for.
bio (Optional[str]): A brief user biography.
avatar (Optional[str]): A URL to an avatar image for the user.
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
@@ -67,54 +59,47 @@ class Resource(CoreResource):
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).
Fetches collection the current authenticated user's activity
as filtered by given parameters
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
Args:
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): Filter results to a single action type.
before (Optional[datetime]): Latest cutoff for activity to include.
after (Optional[datetime]): Oldest cutoff for an activity to include.
cursor (Optional[datetime]): Timestamp cursor for pagination.
Returns:
Activity collection, filtered according to the provided parameters.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
return super().activity(limit, action_type, before, after, cursor)
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
"""Fetches all of the current user's pending stream invitations.
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
List[PendingStreamCollaborator]: A list of pending stream invitations.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invites All Get"})
metrics.track(
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
)
return super().get_all_pending_invites()
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.
"""Fetches a specific pending invite for the current user on a given stream.
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)
Args:
stream_id (str): The ID of the stream to look for invites on.
token (Optional[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)
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
+6 -7
View File
@@ -1,12 +1,9 @@
from typing import Optional
from gql import gql
from typing import Optional, Union
from specklepy.api.models import Branch
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.core.api.resources.branch import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
@@ -35,7 +32,9 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
return super().create(stream_id, name, description)
def get(self, stream_id: str, name: str, commits_limit: int = 10):
def get(
self, stream_id: str, name: str, commits_limit: int = 10
) -> Union[Branch, None, SpeckleException]:
"""Get a branch by name from a stream
Arguments:
+8 -9
View File
@@ -1,12 +1,9 @@
from typing import List, Optional
from gql import gql
from typing import List, Optional, Union
from specklepy.api.models import Commit
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.core.api.resources.commit import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
@@ -55,8 +52,8 @@ class Resource(CoreResource):
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: List[str] = None,
) -> str:
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
"""
Creates a commit on a branch
@@ -76,7 +73,9 @@ class Resource(CoreResource):
str -- the id of the created commit
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Create"})
return super().create(stream_id, object_id, branch_name, message, source_application, parents)
return super().create(
stream_id, object_id, branch_name, message, source_application, parents
)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
+2 -8
View File
@@ -1,13 +1,8 @@
from typing import Dict, List
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.objects.base import Base
from specklepy.logging import metrics
from specklepy.core.api.resources.object import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.objects.base import Base
class Resource(CoreResource):
@@ -58,4 +53,3 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "Object Create"})
return super().create(stream_id, objects)
+34 -34
View File
@@ -1,18 +1,18 @@
from datetime import datetime, timezone
from datetime import datetime
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.core.api.resources.other_user import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.other_user import Resource as CoreResource
class Resource(CoreResource):
"""API Access class for other users, that are not the currently active user."""
"""
Provides API access to other users' profiles and activities on the platform.
This class enables fetching limited information about users, searching for users by name or email,
and accessing user activity logs with appropriate privacy and access control measures in place.
"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -25,13 +25,13 @@ class Resource(CoreResource):
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
Retrieves the profile of a user specified by their user ID.
Arguments:
id {str} -- the user id
Args:
id (str): The unique identifier of the user.
Returns:
LimitedUser -- the retrieved profile of another user
LimitedUser: The profile of the user with limited information.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
@@ -39,22 +39,25 @@ class Resource(CoreResource):
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
"""
Searches for users by name or email.
The search requires a minimum query length of 3 characters.
Args:
search_query (str): The search string.
limit (int): Maximum number of search results to return.
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[LimitedUser] -- a list of User objects that match the search query
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
query or an exception if the query is too short.
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
message="User search query must be at least 3 characters."
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
return super().search(search_query, limit)
def activity(
self,
@@ -66,22 +69,19 @@ class Resource(CoreResource):
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.
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
Args:
user_id (str): The ID of the user whose activities are being requested.
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): A specific type of activity to filter.
before (Optional[datetime]): Latest timestamp to include activities before.
after (Optional[datetime]): Earliest timestamp to include activities after.
cursor (Optional[datetime]): Timestamp for pagination cursor.
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
Returns:
ActivityCollection: A collection of user activities filtered according to specified criteria.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
return super().activity(user_id, limit, action_type, before, after, cursor)
+2 -8
View File
@@ -1,14 +1,8 @@
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
from specklepy.core.api.resources.server import Resource as CoreResource
from specklepy.logging import metrics
class Resource(CoreResource):
@@ -73,4 +67,4 @@ class Resource(CoreResource):
bool -- True if the token was successfully deleted
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Revoke Token"})
return super().revoke_token(token)
return super().revoke_token(token)
+8 -11
View File
@@ -1,15 +1,9 @@
from datetime import datetime, timezone
from datetime import datetime
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
from specklepy.api.models import PendingStreamCollaborator, Stream
from specklepy.core.api.resources.stream import Resource as CoreResource
from specklepy.logging import metrics
class Resource(CoreResource):
@@ -256,7 +250,11 @@ class Resource(CoreResource):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Permission Update", "role": role})
metrics.track(
metrics.SDK,
self.account,
{"name": "Stream Permission Update", "role": role},
)
return super().update_permission(stream_id, user_id, role)
def revoke_permission(self, stream_id: str, user_id: str):
@@ -301,4 +299,3 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Activity"})
return super().activity(stream_id, action_type, limit, before, after, cursor)
+8 -7
View File
@@ -1,15 +1,12 @@
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.core.api.resources.subscriptions import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
from specklepy.core.api.resources.subscriptions import Resource as CoreResource
def check_wsclient(function):
@wraps(function)
@@ -64,7 +61,9 @@ class Resource(CoreResource):
Returns:
Stream -- the update stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Updated"})
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Updated"}
)
return super().stream_updated(id, callback)
@check_wsclient
@@ -83,7 +82,9 @@ class Resource(CoreResource):
Returns:
dict -- dict containing 'id' of stream removed and optionally 'revokedBy'
"""
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Removed"})
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Removed"}
)
return super().stream_removed(callback)
@check_wsclient
+11 -16
View File
@@ -1,16 +1,12 @@
from datetime import datetime, timezone
from datetime import datetime
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.exceptions import SpeckleException
from specklepy.logging import metrics
from specklepy.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.user import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = (
@@ -46,7 +42,7 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "User Get_deprecated"})
return super().get(id)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search(
self, search_query: str, limit: int = 25
@@ -83,7 +79,7 @@ class Resource(CoreResource):
Returns:
bool -- True if your profile was updated successfully
"""
#metrics.track(metrics.USER, self.account, {"name": "update"})
# metrics.track(metrics.USER, self.account, {"name": "update"})
metrics.track(metrics.SDK, self.account, {"name": "User Update_deprecated"})
return super().update(name, company, bio, avatar)
@@ -118,7 +114,6 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "User Activity_deprecated"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
@@ -130,10 +125,11 @@ class Resource(CoreResource):
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
#metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(metrics.SDK, self.account, {"name": "User GetAllInvites_deprecated"})
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(
metrics.SDK, self.account, {"name": "User GetAllInvites_deprecated"}
)
return super().get_all_pending_invites()
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite(
@@ -152,7 +148,6 @@ class Resource(CoreResource):
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
#metrics.track(metrics.INVITE, self.account, {"name": "get"})
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(metrics.SDK, self.account, {"name": "User GetInvite_deprecated"})
return super().get_pending_invite(stream_id, token)
+7 -13
View File
@@ -1,17 +1,9 @@
from urllib.parse import unquote, urlparse
from warnings import warn
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
)
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.api.credentials import Account
from specklepy.core.api.wrapper import StreamWrapper as CoreStreamWrapper
from specklepy.logging import metrics
from specklepy.transports.server.server import ServerTransport
from specklepy.logging import metrics
from specklepy.core.api.wrapper import StreamWrapper as CoreStreamWrapper
class StreamWrapper(CoreStreamWrapper):
"""
@@ -51,7 +43,7 @@ class StreamWrapper(CoreStreamWrapper):
_account: Account = None
def __init__(self, url: str) -> None:
super().__init__(url = url)
super().__init__(url=url)
def get_account(self, token: str = None) -> Account:
"""
@@ -90,5 +82,7 @@ class StreamWrapper(CoreStreamWrapper):
ServerTransport -- constructed for this stream
with a pre-authenticated client
"""
metrics.track(metrics.SDK, custom_props={"name": "Stream Wrapper Get Transport"})
metrics.track(
metrics.SDK, custom_props={"name": "Stream Wrapper Get Transport"}
)
return super().get_transport(token)
+21 -7
View File
@@ -11,7 +11,6 @@ from gql.transport.websockets import WebsocketsTransport
from specklepy.core.api import resources
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.core.api.resources import (
user,
active_user,
branch,
commit,
@@ -20,6 +19,7 @@ from specklepy.core.api.resources import (
server,
stream,
subscriptions,
user,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
@@ -59,7 +59,14 @@ class SpeckleClient:
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
connection_retries: int = 3,
connection_timeout: int = 10,
) -> None:
ws_protocol = "ws"
http_protocol = "http"
@@ -74,9 +81,17 @@ class SpeckleClient:
self.graphql = f"{self.url}/graphql"
self.ws_url = f"{ws_protocol}://{host}/graphql"
self.account = Account()
self.verify_certificate = verify_certificate
self.connection_retries = connection_retries
self.connection_timeout = connection_timeout
self.httpclient = Client(
transport=RequestsHTTPTransport(url=self.graphql, verify=True, retries=3)
transport=RequestsHTTPTransport(
url=self.graphql,
verify=self.verify_certificate,
retries=self.connection_retries,
timeout=self.connection_timeout,
)
)
self.wsclient = None
@@ -115,8 +130,7 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.authenticate_with_token(token)
self._set_up_client()
self.authenticate_with_account(get_account_from_token(token))
def authenticate_with_token(self, token: str) -> None:
"""
@@ -127,7 +141,7 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.account = get_account_from_token(token, self.url)
self.account = Account.from_token(token, self.url)
self._set_up_client()
def authenticate_with_account(self, account: Account) -> None:
@@ -150,7 +164,7 @@ class SpeckleClient:
"apollographql-client-version": metrics.HOST_APP_VERSION,
}
httptransport = RequestsHTTPTransport(
url=self.graphql, headers=headers, verify=True, retries=3
url=self.graphql, headers=headers, verify=self.verify_certificate, retries=3
)
wstransport = WebsocketsTransport(
url=self.ws_url,
+24 -2
View File
@@ -1,12 +1,12 @@
import os
from pathlib import Path
from typing import List, Optional
from urllib.parse import urlparse
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from specklepy.core.api.models import ServerInfo
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
@@ -110,7 +110,7 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
if not default:
default = accounts[0]
default.isDefault = True
#metrics.initialise_tracker(default)
# metrics.initialise_tracker(default)
return default
@@ -143,6 +143,28 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
return Account.from_token(token, server_url)
def get_accounts_for_server(host: str) -> List[Account]:
all_accounts = get_local_accounts()
filtered: List[Account] = []
for acc in all_accounts:
moved_from = (
acc.serverInfo.migration.movedFrom if acc.serverInfo.migration else None
)
if moved_from and host == urlparse(moved_from).netloc:
filtered.append(acc)
for acc in all_accounts:
if any([x for x in filtered if x.userInfo.id == acc.userInfo.id]):
continue
if host == urlparse(acc.serverInfo.url).netloc:
filtered.append(acc)
return filtered
class StreamWrapper:
def __init__(self, url: str = None) -> None:
raise SpeckleException(
+7
View File
@@ -185,6 +185,11 @@ class ActivityCollection(BaseModel):
return self.__repr__()
class ServerMigration(BaseModel):
movedTo: Optional[str] = None
movedFrom: Optional[str] = None
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
@@ -196,3 +201,5 @@ class ServerInfo(BaseModel):
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
+1 -1
View File
@@ -1,6 +1,6 @@
from typing import List, Optional
#from specklepy.logging import metrics
# from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
@@ -3,7 +3,11 @@ from typing import List, Optional
from gql import gql
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
User,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
@@ -4,6 +4,7 @@ from gql import gql
from specklepy.core.api.models import Branch
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "branch"
@@ -39,6 +40,8 @@ class Resource(ResourceBase):
}
"""
)
if len(name) < 3:
return SpeckleException(message="Branch Name must be at least 3 characters")
params = {
"branch": {
"streamId": stream_id,
+4 -3
View File
@@ -1,9 +1,10 @@
from typing import List, Optional
from typing import List, Optional, Union
from gql import gql
from specklepy.core.api.models import Commit
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "commit"
@@ -106,8 +107,8 @@ class Resource(ResourceBase):
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: List[str] = None,
) -> str:
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
"""
Creates a commit on a branch
+14 -1
View File
@@ -1,6 +1,7 @@
import re
from typing import Any, Dict, List, Tuple
import requests
from gql import gql
from specklepy.core.api.models import ServerInfo
@@ -56,9 +57,21 @@ class Resource(ResourceBase):
"""
)
return self.make_request(
server_info = self.make_request(
query=query, return_type="serverInfo", schema=ServerInfo
)
if isinstance(server_info, ServerInfo) and isinstance(
server_info.canonicalUrl, str
):
r = requests.get(
server_info.canonicalUrl, headers={"User-Agent": "specklepy SDK"}
)
if "x-speckle-frontend-2" in r.headers:
server_info.frontend2 = True
else:
server_info.frontend2 = False
return server_info
def version(self) -> Tuple[Any, ...]:
"""Get the server version
+14 -11
View File
@@ -1,10 +1,13 @@
from datetime import datetime, timezone
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, Stream
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Stream,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
@@ -163,7 +166,8 @@ class Resource(ResourceBase):
}
"""
)
if len(name) < 3 and len(name) != 0:
return SpeckleException(message="Stream Name must be at least 3 characters")
params = {
"stream": {"name": name, "description": description, "isPublic": is_public}
}
@@ -504,11 +508,10 @@ class Resource(ResourceBase):
user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id}
for user_id in (user_ids if user_ids is not None else [])
for user_id in (user_ids if user_ids is not None else [])
if user_id is not None
]
params = {"input": [*email_invites, *user_invites]}
return self.make_request(
@@ -728,13 +731,13 @@ class Resource(ResourceBase):
"stream_id": stream_id,
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat()
if before
else before,
"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,
"cursor": (
cursor.astimezone(timezone.utc).isoformat() if cursor else cursor
),
}
except AttributeError as e:
raise SpeckleException(
+5 -2
View File
@@ -4,9 +4,12 @@ from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
User,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "user"
+123 -27
View File
@@ -1,11 +1,13 @@
from urllib.parse import unquote, urlparse
from urllib.parse import quote, unquote, urlparse
from warnings import warn
from gql import gql
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
get_accounts_for_server,
)
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
@@ -45,6 +47,7 @@ class StreamWrapper:
commit_id: str = None
object_id: str = None
branch_name: str = None
model_id: str = None
_client: SpeckleClient = None
_account: Account = None
@@ -81,29 +84,86 @@ class StreamWrapper:
" provided."
)
# check for fe2 URL
if "/projects/" in parsed.path:
use_fe2 = True
key_stream = "project"
else:
use_fe2 = False
key_stream = "stream"
while segments:
segment = segments.pop(0)
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
if use_fe2 is False:
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
elif segments and use_fe2 is True:
if segment.lower() == "projects":
self.stream_id = segments.pop(0)
elif segment.lower() == "models":
next_segment = segments.pop(0)
if "," in next_segment:
raise SpeckleException("Multi-model urls are not supported yet")
elif unquote(next_segment).startswith("$"):
raise SpeckleException(
"Federation model urls are not supported"
)
elif len(next_segment) == 32:
self.object_id = next_segment
else:
self.branch_name = unquote(next_segment).split("@")[0]
if "@" in unquote(next_segment):
self.commit_id = unquote(next_segment).split("@")[1]
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
if use_fe2 is True and self.branch_name is not None:
self.model_id = self.branch_name
# get branch name
query = gql(
"""
query Project($project_id: String!, $model_id: String!) {
project(id: $project_id) {
id
model(id: $model_id) {
name
}
}
}
"""
)
self._client = self.get_client()
params = {"project_id": self.stream_id, "model_id": self.model_id}
project = self._client.httpclient.execute(query, params)
try:
self.branch_name = project["project"]["model"]["name"]
except KeyError as ke:
raise SpeckleException("Project model name is not found", ke)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
f"Cannot parse {url} into a stream wrapper class - no {key_stream} id found."
)
@property
@@ -118,14 +178,7 @@ class StreamWrapper:
if self._account and self._account.token:
return self._account
self._account = next(
(
a
for a in get_local_accounts()
if self.host == urlparse(a.serverInfo.url).netloc
),
None,
)
self._account = next(iter(get_accounts_for_server(self.host)), None)
if not self._account:
self._account = get_account_from_token(token, self.server_url)
@@ -184,3 +237,46 @@ class StreamWrapper:
if not self._account or not self._account.token:
self.get_account(token)
return ServerTransport(self.stream_id, account=self._account)
def to_string(self) -> str:
"""
Constructs a URL depending on the StreamWrapper type and FE version.
"""
use_fe2 = False
key_streams = "/streams/"
key_branches = "/branches/"
if isinstance(self.branch_name, str):
value_branch = quote(self.branch_name)
if self.branch_name == "globals":
key_branches = "/"
key_commits = "/commits/"
if isinstance(self.commit_id, str) and self.branch_name == "globals":
key_commits = "/globals/"
key_objects = "/objects/"
if "/projects/" in self.stream_url:
use_fe2 = True
key_streams = "/projects/"
key_branches = "/models/"
value_branch = self.model_id
key_commits = "@"
key_objects = "/models/"
wrapper_type = self.type
if use_fe2 is False or (use_fe2 is True and not self.model_id):
base_url = f"{self.server_url}{key_streams}{self.stream_id}"
else: # fe2 is True and model_id available
base_url = f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
if wrapper_type == "object":
return f"{base_url}{key_objects}{self.object_id}"
elif wrapper_type == "commit":
return f"{base_url}{key_commits}{self.commit_id}"
elif wrapper_type == "branch":
return f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
elif wrapper_type == "stream":
return f"{self.server_url}{key_streams}{self.stream_id}"
else:
raise SpeckleException(
f"Cannot parse StreamWrapper of type '{wrapper_type}'"
)
+2 -2
View File
@@ -28,7 +28,7 @@ CONNECTOR = "Connector Action"
RECEIVE = "Receive"
SEND = "Send"
# not in use since 2.15
# not in use since 2.15
ACCOUNTS = "Get Local Accounts"
BRANCH = "Branch Action"
CLIENT = "Speckle Client"
@@ -142,7 +142,7 @@ class MetricsTracker(metaclass=Singleton):
def hash(self, value: str):
inputList = value.lower().split("://")
input = inputList[len(inputList)-1].split("/")[0].split('?')[0]
input = inputList[len(inputList) - 1].split("/")[0].split("?")[0]
return hashlib.md5(input.encode("utf-8")).hexdigest().upper()
def _send_tracking_requests(self):
+3 -4
View File
@@ -1,5 +1,6 @@
from typing import Optional
from specklepy.objects import Base
from specklepy.objects.base import Base
class CRS(Base, speckle_type="Objects.GIS.CRS"):
@@ -8,9 +9,7 @@ class CRS(Base, speckle_type="Objects.GIS.CRS"):
name: Optional[str] = None
authority_id: Optional[str] = None
wkt: Optional[str] = None
units_native: Optional[str] = None
units_native: Optional[str] = None
offset_x: Optional[float] = None
offset_y: Optional[float] = None
rotation: Optional[float] = None
+14 -14
View File
@@ -1,22 +1,22 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.layers import (
VectorLayer,
RasterLayer,
)
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.GIS.geometry import (
GisPolygonGeometry,
GisPolygonElement,
GisLineElement,
GisPointElement,
GisPolygonElement,
GisPolygonGeometry,
GisRasterElement,
)
from specklepy.objects.GIS.layers import RasterLayer, VectorLayer
from specklepy.objects.GIS.CRS import (
CRS,
)
__all__ = ["VectorLayer", "RasterLayer",
"GisPolygonGeometry", "GisPolygonElement", "GisLineElement", "GisPointElement", "GisRasterElement",
"CRS"]
__all__ = [
"VectorLayer",
"RasterLayer",
"GisPolygonGeometry",
"GisPolygonElement",
"GisLineElement",
"GisPointElement",
"GisRasterElement",
"CRS",
]
+34 -13
View File
@@ -1,35 +1,51 @@
from typing import List, Optional, Union
from typing import Optional, Union, List
from specklepy.objects.geometry import Point, Line, Polyline, Circle, Arc, Polycurve, Mesh
from specklepy.objects import Base
from deprecated import deprecated
from specklepy.objects.base import Base
from specklepy.objects.geometry import (
Arc,
Circle,
Line,
Mesh,
Point,
Polycurve,
Polyline,
)
class GisPolygonGeometry(Base, speckle_type="Objects.GIS.PolygonGeometry", detachable={"displayValue"}):
class GisPolygonGeometry(
Base, speckle_type="Objects.GIS.PolygonGeometry", detachable={"displayValue"}
):
"""GIS Polygon Geometry"""
boundary: Optional[Union[Polyline, Arc, Line, Circle, Polycurve]] = None
voids: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]] ] = None
voids: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None
displayValue: Optional[List[Mesh]] = None
class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"):
"""GIS Polygon element"""
geometry: Optional[List[GisPolygonGeometry]] = None
attributes: Optional[Base] = None
class GisLineElement(Base, speckle_type="Objects.GIS.LineElement"):
"""GIS Polyline element"""
geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None,
attributes: Optional[Base] = None,
geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None
attributes: Optional[Base] = None
class GisPointElement(Base, speckle_type="Objects.GIS.PointElement"):
"""GIS Point element"""
geometry: Optional[List[Point]] = None,
attributes: Optional[Base] = None,
geometry: Optional[List[Point]] = None
attributes: Optional[Base] = None
class GisRasterElement(Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}):
class GisRasterElement(
Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}
):
"""GIS Raster element"""
band_count: Optional[int] = None
@@ -43,11 +59,16 @@ class GisRasterElement(Base, speckle_type="Objects.GIS.RasterElement", detachabl
noDataValue: Optional[List[float]] = None
displayValue: Optional[List[Mesh]] = None
class GisTopography(GisRasterElement, speckle_type="Objects.GIS.GisTopography", detachable={"displayValue"}):
class GisTopography(
GisRasterElement,
speckle_type="Objects.GIS.GisTopography",
detachable={"displayValue"},
):
"""GIS Raster element with 3d Topography representation"""
class GisNonGeometryElement(Base, speckle_type="Objects.GIS.NonGeometryElement"):
"""GIS Table feature"""
attributes: Optional[Base] = None
+83 -24
View File
@@ -1,23 +1,26 @@
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Union
from deprecated import deprecated
from specklepy.objects.base import Base
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.other import Collection
from specklepy.objects.GIS.CRS import CRS
from deprecated import deprecated
@deprecated(version="2.15", reason="Use VectorLayer or RasterLayer instead")
class Layer(Base, detachable={"features"}):
"""A GIS Layer"""
def __init__(
self,
name:str=None,
crs:CRS=None,
name: Optional[str] = None,
crs: Optional[CRS] = None,
units: str = "m",
features: Optional[List[Base]] = None,
layerType: str = "None",
geomType: str = "None",
renderer: Optional[dict[str, Any]] = None,
**kwargs
renderer: Optional[Dict[str, Any]] = None,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.name = name
@@ -26,25 +29,27 @@ class Layer(Base, detachable={"features"}):
self.type = layerType
self.features = features or []
self.geomType = geomType
self.renderer = renderer or {}
self.renderer = renderer or {}
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class VectorLayer(
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.VectorLayer",
serialize_ignore={"features"}):
Collection,
detachable={"elements"},
speckle_type="VectorLayer",
serialize_ignore={"features"},
):
"""GIS Vector Layer"""
name: Optional[str]=None
crs: Optional[CRS]=None
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
elements: Optional[List[Base]] = None
attributes: Optional[Base] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
@@ -54,24 +59,25 @@ class VectorLayer(
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class RasterLayer(
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.RasterLayer",
serialize_ignore={"features"}):
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class RasterLayer(
Collection,
detachable={"elements"},
speckle_type="RasterLayer",
serialize_ignore={"features"},
):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[CRS]=None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
rasterCrs: Optional[CRS]=None
rasterCrs: Optional[Union[CRS, Base]] = None
elements: Optional[List[Base]] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
@@ -81,3 +87,56 @@ class RasterLayer(
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class VectorLayer( # noqa: F811
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.VectorLayer",
serialize_ignore={"features"},
):
"""GIS Vector Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
elements: Optional[List[Base]] = None
attributes: Optional[Base] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class RasterLayer( # noqa: F811
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.RasterLayer",
serialize_ignore={"features"},
):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]] = None
elements: Optional[List[Base]] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
+19 -2
View File
@@ -1,6 +1,23 @@
"""Builtin Speckle object kit."""
from specklepy.objects import encoding, geometry, other, primitive, structural, units
from specklepy.objects import (
GIS,
encoding,
geometry,
other,
primitive,
structural,
units,
)
from specklepy.objects.base import Base
__all__ = ["Base", "encoding", "geometry", "other", "units", "structural", "primitive"]
__all__ = [
"Base",
"encoding",
"geometry",
"other",
"units",
"structural",
"primitive",
"GIS",
]
+3 -2
View File
@@ -19,7 +19,7 @@ from warnings import warn
from stringcase import pascalcase
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.units import Units, get_units_from_string
from specklepy.objects.units import Units
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
@@ -188,7 +188,8 @@ class _RegisteringBase:
cls._detachable = cls._detachable.union(detachable)
if serialize_ignore:
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
super().__init_subclass__(**kwargs)
# we know, that the super here is object, that takes no args on init subclass
return super().__init_subclass__()
# T = TypeVar("T")
+14 -14
View File
@@ -40,9 +40,9 @@ class Point(Base, speckle_type=GEOMETRY + "Point"):
class Pointcloud(
Base,
Base,
speckle_type=GEOMETRY + "Pointcloud",
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
):
points: Optional[List[float]] = None
colors: Optional[List[int]] = None
@@ -303,15 +303,15 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
class SpiralType(Enum):
Biquadratic = (0,)
BiquadraticParabola = (1,)
Bloss = (2,)
Clothoid = (3,)
Cosine = (4,)
Cubic = (5,)
CubicParabola = (6,)
Radioid = (7,)
Sinusoid = (8,)
Biquadratic = 0
BiquadraticParabola = 1
Bloss = 2
Clothoid = 3
Cosine = 4
Cubic = 5
CubicParabola = 6
Radioid = 7
Sinusoid = 8
Unknown = 9
@@ -319,7 +319,7 @@ class Spiral(Base, speckle_type=GEOMETRY + "Spiral", detachable={"displayValue"}
startPoint: Optional[Point] = None
endPoint: Optional[Point]
plane: Optional[Plane]
turns: Optional[int]
turns: Optional[float]
pitchAxis: Optional[Vector] = Vector()
pitch: float = 0
spiralType: Optional[SpiralType] = None
@@ -898,7 +898,7 @@ class Brep(
def VerticesValue(self) -> List[Point]:
if self.Vertices is None:
return None
encoded_unit = get_encoding_from_units(self.Vertices[0]._units)
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
values = [encoded_unit]
for vertex in self.Vertices:
values.extend(vertex.to_list())
@@ -913,7 +913,7 @@ class Brep(
for i in range(0, len(value), 3):
vertex = Point.from_list(value[i : i + 3])
vertex._units = units
vertex.units = units
vertices.append(vertex)
self.Vertices = vertices
@@ -1,24 +1,25 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Collection, Dict, Generic, Iterable, List, Optional, Tuple, TypeVar
from typing import Any, Collection, Dict, Generic, Iterable, Optional, Tuple, TypeVar
from attrs import define
from specklepy.objects.base import Base
ROOT: str = "__Root"
T = TypeVar('T')
T = TypeVar("T")
PARENT_INFO = Tuple[Optional[str], str]
@define(slots=True)
class CommitObjectBuilder(ABC, Generic[T]):
converted: Dict[str, Base]
_parent_infos: Dict[str, Collection[PARENT_INFO]]
def __init__(self) -> None:
self.converted = {}
self._parent_infos = {}
@abstractmethod
def include_object(self, conversion_result: Base, native_object: T) -> None:
pass
@@ -26,14 +27,17 @@ class CommitObjectBuilder(ABC, Generic[T]):
def build_commit_object(self, root_commit_object: Base) -> None:
self.apply_relationships(self.converted.values(), root_commit_object)
def set_relationship(self, app_id: Optional[str], *parent_info : PARENT_INFO) -> None:
def set_relationship(
self, app_id: Optional[str], *parent_info: PARENT_INFO
) -> None:
if not app_id:
return
self._parent_infos[app_id] = parent_info
def apply_relationships(self, to_add: Iterable[Base], root_commit_object: Base) -> None:
def apply_relationships(
self, to_add: Iterable[Base], root_commit_object: Base
) -> None:
for c in to_add:
try:
self.apply_relationship(c, root_commit_object)
@@ -41,20 +45,25 @@ class CommitObjectBuilder(ABC, Generic[T]):
print(f"Failed to add object {type(c)} to commit object: {ex}")
def apply_relationship(self, current: Base, root_commit_object: Base):
if not current.applicationId: raise Exception(f"Expected applicationId to have been set")
if not current.applicationId:
raise Exception("Expected applicationId to have been set")
parents = self._parent_infos[current.applicationId]
for (parent_id, prop_name) in parents:
if not parent_id: continue
for parent_id, prop_name in parents:
if not parent_id:
continue
parent: Optional[Base]
if parent_id == ROOT:
parent = root_commit_object
else:
parent = self.converted[parent_id] if parent_id in self.converted else None
if not parent: continue
parent = (
self.converted[parent_id] if parent_id in self.converted else None
)
if not parent:
continue
try:
elements = get_detached_prop(parent, prop_name)
@@ -66,18 +75,26 @@ class CommitObjectBuilder(ABC, Generic[T]):
return
except Exception as ex:
# A parent was found, but it was invalid (Likely because of a type mismatch on a `elements` property)
print(f"Failed to add object {type(current)} to a converted parent; {ex}")
raise Exception(f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!")
print(
f"Failed to add object {type(current)} to a converted parent; {ex}"
)
raise Exception(
f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!"
)
def get_detached_prop(speckle_object: Base, prop_name: str) -> Optional[Any]:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
return getattr(speckle_object, detached_prop_name, None)
def set_detached_prop(speckle_object: Base, prop_name: str, value: Optional[Any]) -> None:
def set_detached_prop(
speckle_object: Base, prop_name: str, value: Optional[Any]
) -> None:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
setattr(speckle_object, detached_prop_name, value)
def get_detached_prop_name(speckle_object: Base, prop_name: str) -> str:
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
@@ -3,7 +3,7 @@ from typing import Any, Callable, Collection, Iterable, Iterator, List, Optional
from attrs import define
from typing_extensions import Protocol, final
from specklepy.objects import Base
from specklepy.objects.base import Base
class ITraversalRule(Protocol):
@@ -41,7 +41,6 @@ class TraversalContext:
@final
@define(slots=True, frozen=True)
class GraphTraversal:
_rules: List[ITraversalRule]
def traverse(self, root: Base) -> Iterator[TraversalContext]:
@@ -58,15 +57,15 @@ class GraphTraversal:
members_to_traverse = active_rule.get_members_to_traverse(current)
for child_prop in members_to_traverse:
try:
if child_prop in {"speckle_type", "units", "applicationId"}: continue #debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
if child_prop in {"speckle_type", "units", "applicationId"}:
continue # debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
if getattr(current, child_prop, None):
value = current[child_prop]
self._traverse_member_to_stack(
stack, value, child_prop, head
)
except KeyError as ex:
self._traverse_member_to_stack(stack, value, child_prop, head)
except KeyError:
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
pass
@staticmethod
def _traverse_member_to_stack(
stack: List[TraversalContext],
@@ -78,10 +77,14 @@ class GraphTraversal:
stack.append(TraversalContext(value, member_name, parent))
elif isinstance(value, list):
for obj in value:
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
GraphTraversal._traverse_member_to_stack(
stack, obj, member_name, parent
)
elif isinstance(value, dict):
for obj in value.values():
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
GraphTraversal._traverse_member_to_stack(
stack, obj, member_name, parent
)
@staticmethod
def traverse_member(value: Optional[Any]) -> Iterator[Base]:
@@ -96,7 +99,6 @@ class GraphTraversal:
for o in GraphTraversal.traverse_member(obj):
yield o
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
return self._get_active_rule(o) or _default_rule
@@ -120,4 +122,4 @@ class TraversalRule:
for condition in self._conditions:
if condition(o):
return True
return False
return False
+23 -10
View File
@@ -1,7 +1,8 @@
from typing import Any, List, Optional
from deprecated import deprecated
from specklepy.objects.geometry import Point, Vector
from specklepy.objects.geometry import Plane, Point, Polyline, Vector
from .base import Base
@@ -71,6 +72,19 @@ class DisplayStyle(Base, speckle_type=OTHER + "DisplayStyle"):
lineweight: float = 0
class Text(Base, speckle_type=OTHER + "Text"):
"""
Text object to render it on viewer.
"""
plane: Plane
value: str
height: float
rotation: float
displayValue: Optional[List[Polyline]] = None
richText: Optional[str] = None
class Transform(
Base,
speckle_type=OTHER + "Transform",
@@ -247,9 +261,7 @@ class BlockDefinition(
geometry: Optional[List[Base]] = None
class Instance(
Base, speckle_type=OTHER + "Instance", detachable={"definition"}
):
class Instance(Base, speckle_type=OTHER + "Instance", detachable={"definition"}):
transform: Optional[Transform] = None
definition: Optional[Base] = None
@@ -268,17 +280,17 @@ class BlockInstance(
def blockDefinition(self, value: Optional[BlockDefinition]) -> None:
self.definition = value
class RevitInstance(Instance, speckle_type=OTHER_REVIT + "RevitInstance"):
level: Optional[Base] = None
level: Optional[Base] = None
facingFlipped: bool
handFlipped: bool
parameters: Optional[Base] = None
parameters: Optional[Base] = None
elementId: Optional[str]
# TODO: prob move this into a built elements module, but just trialling this for now
class RevitParameter(
Base, speckle_type="Objects.BuiltElements.Revit.Parameter"
):
class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter"):
name: Optional[str] = None
value: Any = None
applicationUnitType: Optional[str] = None # eg UnitType UT_Length
@@ -290,9 +302,10 @@ class RevitParameter(
isReadOnly: bool = False
isTypeParameter: bool = False
class Collection(
Base, speckle_type="Speckle.Core.Models.Collection", detachable={"elements"}
):
name: Optional[str] = None
collectionType: Optional[str] = None
elements: Optional[List[Base]] = None
elements: Optional[List[Base]] = None
+1 -4
View File
@@ -6,10 +6,7 @@ from specklepy.objects.structural.analysis import (
ModelSettings,
ModelUnits,
)
from specklepy.objects.structural.axis import (
AxisType,
Axis
)
from specklepy.objects.structural.axis import Axis, AxisType
from specklepy.objects.structural.geometry import (
Element1D,
Element2D,
+1 -1
View File
@@ -1,5 +1,5 @@
from typing import Optional
from enum import Enum
from typing import Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry import Plane
+4 -2
View File
@@ -106,7 +106,9 @@ def get_encoding_from_units(unit: Union[Units, str, None]):
def get_scale_factor_from_string(fromUnits: str, toUnits: str) -> float:
"""Returns a scalar to convert distance values from one unit system to another"""
return get_scale_factor(get_units_from_string(fromUnits), get_units_from_string(toUnits))
return get_scale_factor(
get_units_from_string(fromUnits), get_units_from_string(toUnits)
)
def get_scale_factor(fromUnits: Units, toUnits: Units) -> float:
@@ -119,4 +121,4 @@ def get_scale_factor_to_meters(fromUnits: Units) -> float:
if fromUnits not in UNIT_SCALE:
raise ValueError(f"Invalid units provided: {fromUnits}")
return UNIT_SCALE[fromUnits]
return UNIT_SCALE[fromUnits]
+1 -1
View File
@@ -1,4 +1,4 @@
from typing import Any, Dict, List
from typing import Dict, List
from specklepy.transports.abstract_transport import AbstractTransport
@@ -137,7 +137,7 @@ class BatchSender(object):
raise SpeckleException(
message=(
"Could not save the object to the server - status code"
f" {r.status_code} ({r.text[:1000]})"
f" {r.status_code} ({r.text[:1000]})"
)
)
except json.JSONDecodeError as error:
+13 -9
View File
@@ -1,5 +1,5 @@
import json
from typing import Any, Dict, List, Optional
from typing import Dict, List, Optional
from warnings import warn
import requests
@@ -73,7 +73,7 @@ class ServerTransport(AbstractTransport):
warn(
SpeckleWarning(
"Unauthenticated Speckle Client provided to Server Transport"
f" for {self.url}. Receiving from private streams will fail."
f" for {url}. Receiving from private streams will fail."
)
)
else:
@@ -84,14 +84,18 @@ class ServerTransport(AbstractTransport):
self.stream_id = stream_id
self.url = url
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
self.session = requests.Session()
self.session.headers.update(
{"Authorization": f"Bearer {self.account.token}", "Accept": "text/plain"}
)
if self.account is not None:
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
self.session.headers.update(
{
"Authorization": f"Bearer {self.account.token}",
"Accept": "text/plain",
}
)
@property
def name(self) -> str:
+1 -1
View File
@@ -1,7 +1,7 @@
import os
import sqlite3
from contextlib import closing
from typing import Any, Dict, List, Optional, Tuple
from typing import Dict, List, Optional, Tuple
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging.exceptions import SpeckleException
@@ -0,0 +1,300 @@
"""Run integration tests with a speckle server."""
import os
from pathlib import Path
from typing import Dict
import pytest
from gql import gql
from speckle_automate import (
AutomationContext,
AutomationRunData,
AutomationStatus,
run_function,
)
from speckle_automate.helpers import crypto_random_string, register_new_automation
from speckle_automate.schema import AutomateBase
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.transports.server import ServerTransport
@pytest.fixture
def speckle_token(user_dict: Dict[str, str]) -> str:
"""Provide a speckle token for the test suite."""
return user_dict["token"]
@pytest.fixture
def speckle_server_url(host: str) -> str:
"""Provide a speckle server url for the test suite, default to localhost."""
return f"http://{host}"
@pytest.fixture
def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
"""Initialize a SpeckleClient for testing."""
test_client = SpeckleClient(speckle_server_url, use_ssl=False)
test_client.authenticate_with_token(speckle_token)
return test_client
@pytest.fixture
def test_object() -> Base:
"""Create a Base model for testing."""
root_object = Base()
root_object.foo = "bar"
return root_object
@pytest.fixture
def automation_run_data(
test_object: Base, test_client: SpeckleClient, speckle_server_url: str
) -> AutomationRunData:
"""Set up an automation context for testing."""
project_id = test_client.stream.create("Automate function e2e test")
branch_name = "main"
model = test_client.branch.get(project_id, branch_name, commits_limit=1)
model_id: str = model.id
root_obj_id = operations.send(
test_object, [ServerTransport(project_id, test_client)]
)
version_id = test_client.commit.create(project_id, root_obj_id)
automation_name = crypto_random_string(10)
automation_id = crypto_random_string(10)
automation_revision_id = crypto_random_string(10)
register_new_automation(
test_client,
project_id,
model_id,
automation_id,
automation_name,
automation_revision_id,
)
automation_run_id = crypto_random_string(10)
function_id = crypto_random_string(10)
function_name = f"automate test {crypto_random_string(3)}"
return AutomationRunData(
project_id=project_id,
model_id=model_id,
branch_name=branch_name,
version_id=version_id,
speckle_server_url=speckle_server_url,
automation_id=automation_id,
automation_revision_id=automation_revision_id,
automation_run_id=automation_run_id,
function_id=function_id,
function_name=function_name,
function_logo=None,
)
@pytest.fixture
def automation_context(
automation_run_data: AutomationRunData, speckle_token: str
) -> AutomationContext:
"""Set up the run context."""
return AutomationContext.initialize(automation_run_data, speckle_token)
def get_automation_status(
project_id: str,
model_id: str,
speckle_client: SpeckleClient,
):
query = gql(
"""
query AutomationRuns(
$projectId: String!
$modelId: String!
)
{
project(id: $projectId) {
model(id: $modelId) {
automationStatus {
id
status
statusMessage
automationRuns {
id
automationId
versionId
createdAt
updatedAt
status
functionRuns {
id
functionId
elapsed
status
contextView
statusMessage
results
resultVersions {
id
}
}
}
}
}
}
}
"""
)
params = {
"projectId": project_id,
"modelId": model_id,
}
response = speckle_client.httpclient.execute(query, params)
return response["project"]["model"]["automationStatus"]
class FunctionInputs(AutomateBase):
forbidden_speckle_type: str
def automate_function(
automation_context: AutomationContext,
function_inputs: FunctionInputs,
) -> None:
"""Hey, trying the automate sdk experience here."""
version_root_object = automation_context.receive_version()
count = 0
if version_root_object.speckle_type == function_inputs.forbidden_speckle_type:
if not version_root_object.id:
raise ValueError("Cannot operate on objects without their id's.")
automation_context.attach_error_to_objects(
"Forbidden speckle_type",
version_root_object.id,
"This project should not contain the type: "
f"{function_inputs.forbidden_speckle_type}",
)
count += 1
if count > 0:
automation_context.mark_run_failed(
"Automation failed: "
f"Found {count} object that have a forbidden speckle type: "
f"{function_inputs.forbidden_speckle_type}"
)
else:
automation_context.mark_run_success("No forbidden types found.")
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_function_run(automation_context: AutomationContext) -> None:
"""Run an integration test for the automate function."""
automation_context = run_function(
automation_context,
automate_function,
FunctionInputs(forbidden_speckle_type="Base"),
)
assert automation_context.run_status == AutomationStatus.FAILED
status = get_automation_status(
automation_context.automation_run_data.project_id,
automation_context.automation_run_data.model_id,
automation_context.speckle_client,
)
assert status["status"] == automation_context.run_status
status_message = status["automationRuns"][0]["functionRuns"][0]["statusMessage"]
assert status_message == automation_context.status_message
@pytest.fixture
def test_file_path():
path = Path(f"./{crypto_random_string(10)}").resolve()
yield path
os.remove(path)
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_file_uploads(
automation_run_data: AutomationRunData, speckle_token: str, test_file_path: Path
):
"""Test file store capabilities of the automate sdk."""
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
test_file_path.write_text("foobar")
automation_context.store_file_result(test_file_path)
assert len(automation_context._automation_result.blobs) == 1
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_create_version_in_project_raises_error_for_same_model(
automation_context: AutomationContext,
) -> None:
with pytest.raises(ValueError):
automation_context.create_new_version_in_project(
Base(), automation_context.automation_run_data.branch_name
)
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_create_version_in_project(
automation_context: AutomationContext,
) -> None:
root_object = Base()
root_object.foo = "bar"
model_id, version_id = automation_context.create_new_version_in_project(
root_object, "foobar"
)
assert model_id is not None
assert version_id is not None
@pytest.mark.skip(
"currently the function run cannot be integration tested with the server"
)
def test_set_context_view(automation_context: AutomationContext) -> None:
automation_context.set_context_view()
assert automation_context.context_view is not None
assert automation_context.context_view.endswith(
f"models/{automation_context.automation_run_data.model_id}@{automation_context.automation_run_data.version_id}"
)
automation_context.report_run_status()
automation_context._automation_result.result_view = None
dummy_context = "foo@bar"
automation_context.set_context_view([dummy_context])
assert automation_context.context_view is not None
assert automation_context.context_view.endswith(
f"models/{automation_context.automation_run_data.model_id}@{automation_context.automation_run_data.version_id},{dummy_context}"
)
automation_context.report_run_status()
automation_context._automation_result.result_view = None
dummy_context = "foo@baz"
automation_context.set_context_view(
[dummy_context], include_source_model_version=False
)
assert automation_context.context_view is not None
assert automation_context.context_view.endswith(f"models/{dummy_context}")
automation_context.report_run_status()
@@ -18,6 +18,7 @@ class TestServer:
server = client.server.get()
assert isinstance(server, ServerInfo)
assert isinstance(server.frontend2, bool)
def test_server_version(self, client: SpeckleClient):
version = client.server.version()
@@ -8,11 +8,7 @@ from specklepy.api.models import (
Stream,
User,
)
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
UnsupportedException,
)
from specklepy.logging.exceptions import GraphQLException, SpeckleException
@pytest.mark.run(order=3)
@@ -48,6 +44,14 @@ class TestStream:
assert isinstance(stream.id, str)
def test_stream_create_short_name(self, client, stream, updated_stream):
new_stream_id = client.stream.create(
name="x",
description=stream.description,
is_public=stream.isPublic,
)
assert isinstance(new_stream_id, SpeckleException)
def test_stream_get(self, client, stream):
fetched_stream = client.stream.get(stream.id)
@@ -2,11 +2,13 @@ import json
import tempfile
from pathlib import Path
from typing import Iterable
from urllib.parse import unquote
import pytest
from specklepy.api.wrapper import StreamWrapper
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging.exceptions import SpeckleException
@pytest.fixture(scope="module", autouse=True)
@@ -29,6 +31,22 @@ def user_path() -> Iterable[Path]:
speckle_path_provider.override_application_data_path(None)
def test_parse_empty():
try:
StreamWrapper("https://testing.speckle.dev/streams")
assert False
except SpeckleException:
assert True
def test_parse_empty_fe2():
try:
StreamWrapper("https://latest.speckle.systems/projects")
assert False
except SpeckleException:
assert True
def test_parse_stream():
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
assert wrap.type == "stream"
@@ -126,3 +144,72 @@ def test_wrapper_url_match(user_path) -> None:
account = wrap.get_account()
assert account.userInfo.email is None
def test_parse_project():
wrap = StreamWrapper("https://latest.speckle.systems/projects/843d07eb10")
assert wrap.type == "stream"
def test_parse_model():
wrap = StreamWrapper(
"https://latest.speckle.systems/projects/843d07eb10/models/d9eb4918c8"
)
assert wrap.branch_name == "building wrapper"
assert wrap.type == "branch"
def test_parse_federated_model():
try:
StreamWrapper("https://latest.speckle.systems/projects/843d07eb10/models/$main")
assert False
except SpeckleException:
assert True
def test_parse_multi_model():
try:
StreamWrapper(
"https://latest.speckle.systems/projects/2099ac4b5f/models/1870f279e3,a9cfdddc79"
)
assert False
except SpeckleException:
assert True
def test_parse_object_fe2():
wrap = StreamWrapper(
"https://latest.speckle.systems/projects/24c3741255/models/b48d1b10f5a732f4ca4144286391282c"
)
assert wrap.type == "object"
def test_parse_version():
wrap = StreamWrapper(
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838@c42d5cbac1"
)
wrap_quoted = StreamWrapper(
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838%40c42d5cbac1"
)
assert wrap.type == "commit"
assert wrap_quoted.type == "commit"
def test_to_string():
urls = [
"https://testing.speckle.dev/streams/a75ab4f10f",
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F",
"https://testing.speckle.dev/streams/0c6ad366c4/globals",
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893",
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792",
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6",
"https://latest.speckle.systems/projects/843d07eb10",
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838",
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838@c42d5cbac1",
"https://latest.speckle.systems/projects/843d07eb10/models/4e7345c838%40c42d5cbac1",
"https://latest.speckle.systems/projects/24c3741255/models/b48d1b10f5a732f4ca4144286391282c",
]
for url in urls:
wrap = StreamWrapper(url)
assert unquote(wrap.to_string()) == unquote(url)
@@ -0,0 +1,77 @@
import os
import uuid
from typing import List, Optional, Tuple
from urllib.parse import urlparse
import pytest
from specklepy.core.api.credentials import Account, UserInfo, get_accounts_for_server
from specklepy.core.api.models import ServerInfo, ServerMigration
from specklepy.core.helpers import speckle_path_provider
def _create_account(
id: str, url: str, movedFrom: Optional[str], movedTo: Optional[str]
) -> Account:
return Account(
id=uuid.uuid4().hex[:6].lower(),
token="myToken",
serverInfo=ServerInfo(
url=url,
name="myServer",
migration=ServerMigration(movedTo=movedTo, movedFrom=movedFrom),
),
userInfo=UserInfo(id=id),
)
def _test_cases() -> List[Tuple[List[Account], str, List[Account]]]:
user_id_1 = uuid.uuid4().hex[:6].lower()
user_id_2 = uuid.uuid4().hex[:6].lower()
old = _create_account(
user_id_1, "https://old.example.com", None, "https://new.example.com"
)
new = _create_account(
user_id_1, "https://new.example.com", "https://old.example.com", None
)
other = _create_account(user_id_2, "https://other.example.com", None, None)
given_accounts = [old, new, other]
reversed = [other, new, old]
return [
(given_accounts, "https://old.example.com", [new]),
(given_accounts, "https://new.example.com", [new]),
(reversed, "https://old.example.com", [new]),
]
def _clean_accounts(accounts: List[Account]) -> None:
json_accounts = speckle_path_provider.accounts_folder_path()
for acc in accounts:
# deleting acc json file in json_accounts path
os.remove(os.path.join(json_accounts, f"{acc.id}.json"))
pass
def _add_accounts(accounts: List[Account]) -> None:
json_accounts = speckle_path_provider.accounts_folder_path()
for acc in accounts:
data = Account.model_dump_json(acc)
with open(os.path.join(json_accounts, f"{acc.id}.json"), "w") as f:
f.write(data)
@pytest.mark.parametrize("accounts, requested_url, expected", _test_cases())
def test_server_migration(
accounts: List[Account], requested_url: str, expected: List[Account]
) -> None:
_add_accounts(accounts)
try:
res = get_accounts_for_server(urlparse(requested_url).netloc)
assert res == expected
finally:
_clean_accounts(accounts)
+3 -3
View File
@@ -388,9 +388,9 @@ def test_brep_curve3d_values_serialization(curve, polyline, circle):
def test_brep_vertices_values_serialization():
brep = Brep()
brep.VerticesValue = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3]
assert brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, _units=Units.mm).get_id()
assert brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, _units=Units.mm).get_id()
assert brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, _units=Units.mm).get_id()
assert brep.Vertices[0].get_id() == Point(x=1, y=1, z=1, units=Units.mm).get_id()
assert brep.Vertices[1].get_id() == Point(x=2, y=2, z=2, units=Units.mm).get_id()
assert brep.Vertices[2].get_id() == Point(x=3, y=3, z=3, units=Units.mm).get_id()
def test_trims_value_serialization():
+1 -1
View File
@@ -102,4 +102,4 @@ class GraphTraversalTests(TestCase):
self.assertCountEqual(ret, [test_case, expected_traverse, expected_traverse])
self.assertNotIn(expected_ignore, ret)
self.assertEqual(len(ret), 3)
self.assertEqual(len(ret), 3)
+6 -1
View File
@@ -107,7 +107,12 @@ fake_bases = [FakeBase("foo"), FakeBase("bar")]
fake_bases,
),
(List["int"], [2, 3, 4], True, [2, 3, 4]),
(Union[float, Dict[str, float]], {"foo": 1, "bar": 2}, True, {"foo": 1.0, "bar": 2.0}),
(
Union[float, Dict[str, float]],
{"foo": 1, "bar": 2},
True,
{"foo": 1.0, "bar": 2.0},
),
(Union[float, Dict[str, float]], {"foo": "bar"}, False, {"foo": "bar"}),
],
)
+9 -13
View File
@@ -1,5 +1,3 @@
import pytest
from specklepy.objects.units import Units, get_scale_factor
@@ -8,7 +6,7 @@ from specklepy.objects.units import Units, get_scale_factor
@pytest.mark.parametrize(
"fromUnits, toUnits, inValue, expectedOutValue",
[
#To self
# To self
(Units.km, Units.km, 1.5, 1.5),
(Units.km, Units.km, 0, 0),
(Units.m, Units.m, 1.5, 1.5),
@@ -23,24 +21,20 @@ from specklepy.objects.units import Units, get_scale_factor
(Units.yards, Units.yards, 0, 0),
(Units.feet, Units.feet, 1.5, 1.5),
(Units.feet, Units.feet, 0, 0),
#To Meters
# To Meters
(Units.km, Units.m, 987654.321, 987654321),
(Units.m, Units.m, 987654.321, 987654.321),
(Units.mm, Units.m, 98765432.1, 98765.4321),
(Units.cm, Units.m, 9876543.21, 98765.4321),
#To negative meters
# To negative meters
(Units.km, Units.m, -987654.321, -987654321),
(Units.m, Units.m,- 987654.321, -987654.321),
(Units.m, Units.m, -987654.321, -987654.321),
(Units.mm, Units.m, -98765432.1, -98765.4321),
(Units.cm, Units.m, -9876543.21, -98765.4321),
(Units.m, Units.km, 987654.321, 987.654321),
(Units.m, Units.cm, 987654.321, 98765432.1),
(Units.m, Units.mm, 987654.321, 987654321),
#Imperial
# Imperial
(Units.miles, Units.m, 123.45, 198673.517),
(Units.miles, Units.inches, 123.45, 7821792),
(Units.yards, Units.m, 123.45, 112.88268),
@@ -50,7 +44,9 @@ from specklepy.objects.units import Units, get_scale_factor
(Units.inches, Units.m, 123.45, 3.13563),
],
)
def test_get_scale_factor_between_units(fromUnits: Units, toUnits: Units, inValue: float, expectedOutValue: float):
def test_get_scale_factor_between_units(
fromUnits: Units, toUnits: Units, inValue: float, expectedOutValue: float
):
Tolerance = 1e-10
actual = inValue * get_scale_factor(fromUnits, toUnits)
assert(actual - expectedOutValue < Tolerance)
assert actual - expectedOutValue < Tolerance
+14 -19
View File
@@ -3,9 +3,9 @@ Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from importlib import import_module, invalidate_caches
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
_user_data_env_var = "SPECKLE_USERDATA_PATH"
@@ -55,9 +55,7 @@ def user_application_data_path() -> Path:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception(
"Cannot get appdata path from environment."
)
raise Exception("Cannot get appdata path from environment.")
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
@@ -68,9 +66,7 @@ def user_application_data_path() -> Path:
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception(
"Failed to initialize user application data path.", ex
)
raise Exception("Failed to initialize user application data path.", ex)
def user_speckle_folder_path() -> Path:
@@ -90,19 +86,16 @@ def user_speckle_connector_installation_path(host_application: str) -> Path:
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path = user_speckle_connector_installation_path(
host_application
)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
@@ -113,7 +106,6 @@ def connector_installation_path(host_application: str) -> Path:
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
@@ -132,7 +124,9 @@ def ensure_pip() -> None:
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
raise Exception(
f"Failed to install pip, got {completed_process.returncode} return code"
)
def get_requirements_path() -> Path:
@@ -184,7 +178,7 @@ def _import_dependencies() -> None:
# the code above doesn't work for now, it fails on importing graphql-core
# despite that, the connector seams to be working as expected
# But it would be nice to make this solution work
# it would ensure that all dependencies are fully loaded
# it would ensure that all dependencies are fully loaded
# requirements = get_requirements_path().read_text()
# reqs = [
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
@@ -195,6 +189,7 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
@@ -202,6 +197,6 @@ def ensure_dependencies(host_application: str) -> None:
_import_dependencies()
print("Successfully found dependencies")
except ImportError:
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
raise Exception(
f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!"
)