Compare commits

...

204 Commits

Author SHA1 Message Date
KatKatKateryna 89f4c75cc9 formatting by save 2024-12-15 22:36:08 +00:00
KatKatKateryna d20f7ea82a fix 2024-12-15 22:07:41 +00:00
KatKatKateryna 2e44a891e8 formatting to pass tests 2024-12-15 17:06:06 +00:00
Dogukan Karatas edce76491f moves intances under proxies 2024-12-13 15:44:56 +01:00
Dogukan Karatas ecbd0eab09 adds qgis essentials 2024-12-13 15:34:10 +01:00
Gergő Jedlicska b17423b282 Merge pull request #366 from specklesystems/dogu/v3_objects
Object model v3 poc
2024-12-12 16:45:52 +01:00
Gergő Jedlicska 166b0f5e87 Merge branch 'v3-dev' of github.com:specklesystems/specklepy into dogu/v3_objects 2024-12-12 16:44:19 +01:00
Gergő Jedlicska cac34120a9 feat(v3_objects): migrate to full dataclass base objects 2024-12-12 16:41:31 +01:00
Gergő Jedlicska 55c4c68cf3 feat(base): use dataclass for base too 2024-12-12 16:34:42 +01:00
Dogukan Karatas be850d5ea9 updates interface 2024-12-12 15:25:09 +01:00
Dogukan Karatas c9a5badac1 implements IHasUnits 2024-12-11 14:55:00 +01:00
Gergő Jedlicska 118fa07e37 Merge branch 'main' of github.com:specklesystems/specklepy into v3-dev 2024-12-11 14:23:53 +01:00
Gergő Jedlicska d71b616e2b Merge pull request #363 from specklesystems/jrm/filter-tests
Added tests for filters
2024-12-11 14:21:54 +01:00
Jedd Morgan 35750f12c5 Merge branch 'main' into jrm/filter-tests 2024-12-11 13:05:18 +00:00
Dogukan Karatas 5730cdcb43 updates geometry classes 2024-12-11 14:04:24 +01:00
Jedd Morgan 82b6dbbe78 isort 2024-12-11 12:09:56 +00:00
Jedd Morgan 883be4b27b reexport inputs 2024-12-11 12:07:37 +00:00
Dogukan Karatas 37e2711a76 adds new object poc 2024-12-10 16:46:05 +01:00
Gergő Jedlicska 8dcc67fb31 Merge pull request #365 from specklesystems/jedd/cxpla-132-update-connectors-usage-of-user-queries-to-use-activeuser
updated active user streams
2024-12-10 15:57:04 +01:00
Jedd Morgan ed84820995 fix 2024-12-10 14:27:19 +00:00
Jedd Morgan 5c3dcb7bc0 updated active user streams 2024-12-10 14:14:31 +00:00
Gergő Jedlicska 92732e3c76 Merge pull request #364 from specklesystems/gergo/objectV3
gergo/objectV3
2024-12-10 15:04:19 +01:00
Gergő Jedlicska 903951547d chore(launchConfig): remove pytests args 2024-12-10 15:01:02 +01:00
Gergő Jedlicska 82c3dc9ffb feat(objects): collections and more 2024-12-09 14:29:01 +01:00
Gergő Jedlicska a0e10aae99 Merge pull request #362 from specklesystems/jrm/deps
Updated dependencies
2024-12-09 12:26:56 +01:00
Jedd Morgan bbea2a0d76 Aligned pre-commit-hooks 2024-12-09 11:24:38 +00:00
Jedd Morgan a05ac3479b Black reformat 2024-12-09 11:17:26 +00:00
Jedd Morgan 0bd972945e update dependencies 2024-12-09 11:14:54 +00:00
Jedd Morgan f200544065 Moved UserProjectsFilter to UserInputs 2024-12-09 10:58:31 +00:00
Jedd Morgan 68ce9823ae added filter tests 2024-12-09 10:56:17 +00:00
Mucahit Bilal GOKER a920352407 fix(api): rename 'onlyWithRole' to 'onlyWithRoles' in UserProjectsFilter (#361) 2024-12-06 15:53:10 +00:00
Dogukan Karatas 24bfb6718e adds performance tests 2024-12-06 14:25:07 +01:00
Gergő Jedlicska e63f4b8636 wip rework serialization for new object model 2024-12-06 10:53:05 +01:00
Gergő Jedlicska 47c6bd89af wip re-base 2024-12-06 09:46:05 +01:00
Chuck Driesler bd38dfacc7 fix(automate): include project id in run reporting (#356) 2024-11-26 14:48:43 +00:00
Chuck Driesler 281483f0fc fix(automate): add success result case (#355) 2024-11-21 12:07:48 +00:00
Jedd Morgan 932838de8f Revert collection speckle_type change (#354)
* revert collection speckle_type change

* black
2024-11-18 10:57:52 +00:00
Gergő Jedlicska a0b39e4c64 Merge pull request #353 from specklesystems/jrm/api-fix
Updated version mutation inputs
2024-11-13 12:49:49 +01:00
Jedd Morgan 759cd0ef58 Updated version mutation inputs 2024-11-13 10:46:59 +00:00
Claire Kuang 46c18bbe6b Update README.md to align with main github page (#352) 2024-11-05 10:36:56 +00:00
KatKatKateryna 82d39e66fe Collections namespace, clean PolygonGeometry class (#351)
* collections namespace change

* add all C# GIS classes, deprecate the rest

* deprecate GisPolygonGeometry properly

* typo

* add constructors

* add multipatch geometry and units

* reverse new classes

* typos

* formatting

* formatting

* optional collection name

* init fix

* pass applicationId if needed

* remove init - causing all classes inheriting also implement it

* remove init
2024-11-05 10:11:06 +00:00
Gergő Jedlicska 10f7499182 Merge pull request #348 from specklesystems/jedd/cxpla-95-add-fe2-queries-and-mutations-to-specklepy
FE2 API Updates
2024-11-04 18:20:54 +01:00
Jedd Morgan 170d2f0450 isort 2024-10-31 14:04:45 +00:00
Jedd Morgan 040a4e2553 fixed tests 2024-10-31 14:04:20 +00:00
Jedd Morgan e978e4f632 Re-export deprecated resources and models 2024-10-31 13:58:44 +00:00
Jedd Morgan eae60160a1 reverted changes to old subscription resource 2024-10-30 19:45:30 +00:00
Jedd Morgan c78a780e85 Use the correct subscription resource in integration tests 2024-10-30 19:42:43 +00:00
Jedd Morgan 1b45f50697 removed dead code in client 2024-10-30 16:02:38 +00:00
Jedd Morgan be8fae3b1c removed unused subscription functions 2024-10-30 15:55:15 +00:00
Jedd Morgan ab41d3cbe0 last fixes 2024-10-30 15:41:37 +00:00
Jedd Morgan f843bb0c89 Fixed up client auth error handling 2024-10-30 14:32:30 +00:00
Jedd Morgan b7933e0088 pre-commit stuff 2024-10-30 14:11:36 +00:00
Jedd Morgan 7e09d4f4ce pr clean up 2024-10-30 14:10:55 +00:00
Jedd Morgan bb62109332 Fixed subscription tests 2024-10-30 13:51:55 +00:00
Jedd Morgan 3642731f37 Wrapping up tests 2024-10-30 12:43:40 +00:00
Jedd Morgan 3bd849c815 imports 2024-10-28 17:29:23 +00:00
Jedd Morgan 2acf4c41c7 fixed tests 2024-10-28 11:24:46 +00:00
Jedd Morgan 6b6ff80bf2 Fixed issues 2024-10-28 11:02:08 +00:00
Jedd Morgan 0f1f00db00 Other user resource 2024-10-25 13:44:12 +01:00
Jedd Morgan 280927b720 active user update overloads 2024-10-25 12:09:26 +01:00
Jedd Morgan 6096cd25f6 project_invites 2024-10-25 11:30:37 +01:00
Jedd Morgan cc004c8e6b active user 2024-10-24 14:13:19 +01:00
Jedd Morgan a10b2594d3 version resource 2024-10-22 16:34:46 +01:00
Jedd Morgan 976a52bdc8 models 2024-10-21 19:45:30 +01:00
Jedd Morgan 09ca501a74 project integration tests & pydantic serializaiton 2024-10-21 14:14:33 +01:00
Jedd Morgan 225d4f02d4 Merge remote-tracking branch 'origin/main' into jedd/cxpla-95-add-fe2-queries-and-mutations-to-specklepy 2024-10-21 12:25:51 +01:00
Gergő Jedlicska f1b51848cf Merge pull request #349 from specklesystems/jrm/fix-heath-checks
Updated docker compose
2024-10-21 13:23:57 +02:00
Jedd Morgan 08fb3f6cd7 updated docker compose to fe2 2024-10-21 12:16:26 +01:00
Jedd Morgan fe7909c913 trailing whitespace 2024-10-21 11:40:50 +01:00
Jedd Morgan a00e16929d trailing return 2024-10-21 11:36:59 +01:00
Jedd Morgan 44d1ef9f93 re-added frontend service 2024-10-21 11:36:16 +01:00
Jedd Morgan 404dbd1d1e Updated docker compose 2024-10-18 14:03:35 +01:00
Jedd Morgan 537a504121 removed unimplemented file 2024-10-18 14:02:09 +01:00
Jedd Morgan 6c03dc82c8 black + isort 2024-10-18 13:34:23 +01:00
Jedd Morgan 780126528d Added project resource and fe2 models 2024-10-18 13:24:38 +01:00
Gergő Jedlicska fe03d96ae2 Merge pull request #346 from specklesystems/charles/trailingSlash
fix(automate): remove extra slash
2024-08-11 13:31:17 +02:00
Charles Driesler 078a6c8da8 fix(automate): extra slash 2024-08-10 23:17:45 +01:00
Iain Sproat 905377dea1 feat(default domain): app.speckle.systems is now default over speckle.xyz (#343)
- also updates the example email domain to use the IANA owned example domain instead of a production or random domain
2024-07-18 17:19:32 +02:00
Gergő Jedlicska 62c5114cb3 Merge pull request #341 from specklesystems/gergo/fixtures_no_init
fix: remove fixtures from automate exports
2024-06-07 18:53:20 +02:00
Gergő Jedlicska 43a5302a90 fix: tures 2024-06-07 18:51:03 +02:00
Gergő Jedlicska addaa996ea fix: remove fixtures from automate exports 2024-06-07 18:42:19 +02:00
Gergő Jedlicska 3b5421a5bc Merge pull request #340 from specklesystems/gergo/automateExceptionOutcome
feat: add excetion outcome reporting to functions
2024-06-07 15:29:40 +02:00
Gergő Jedlicska 88e8c86fa6 feat: add excetion outcome reporting to functions 2024-06-07 11:13:15 +02:00
Chuck Driesler d6843b9971 Merge pull request #339 from specklesystems/chuck/testAutomationHelpers
WEB-1053 Create helpers for testing automate functions
2024-06-06 12:03:00 +01:00
Charles Driesler 302a9f7f30 repair import 2024-06-06 12:01:00 +01:00
Charles Driesler ede9591c6a export fixtures 2024-06-06 11:58:18 +01:00
Charles Driesler c5b339d891 deps deps deps 2024-06-05 16:59:05 +01:00
Charles Driesler 2e35fb9e5c create helpers for testing functions 2024-06-05 16:38:49 +01:00
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 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
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 6a9f4bf89b gql minimum characters restriction for consistent behavior with frontend 2023-02-08 07:29:02 +08:00
149 changed files with 9524 additions and 3363 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.8.2
- 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.13.2
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 24.10.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
+3 -5
View File
@@ -4,11 +4,9 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
@@ -16,9 +14,9 @@
},
{
"name": "Pytest",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "pytest",
"module": "pytest",
"args": [],
"console": "integratedTerminal",
"justMyCode": true
+6 -36
View File
@@ -2,46 +2,16 @@
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
Speckle | specklepy 🐍
</h1>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
<h3 align="center">
The Python SDK
</h3>
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://github.com/specklesystems/specklepy/"><img src="https://circleci.com/gh/specklesystems/specklepy.svg?style=svg&amp;circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a><a href="https://codecov.io/gh/specklesystems/specklepy">
<img src="https://codecov.io/gh/specklesystems/specklepy/branch/main/graph/badge.svg?token=8KQFL5N0YF"/>
</a> </p>
# About Speckle
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
### Features
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
- **Collaboration:** share your designs collaborate with others
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
- **Real time:** get real time updates and notifications and changes
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
<p align="center"><a href="https://codecov.io/gh/specklesystems/specklepy"><img src="https://codecov.io/gh/specklesystems/specklepy/branch/main/graph/badge.svg?token=8KQFL5N0YF" alt="Codecov"></a></p>
# Repo structure
+11 -53
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:
@@ -52,28 +52,26 @@ services:
####
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend:latest
image: speckle/speckle-frontend-2:latest
restart: always
ports:
- "0.0.0.0:8080:8080"
environment:
FILE_SIZE_LIMIT_MB: 100
speckle-server:
image: speckle/speckle-server:latest
restart: always
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/graphql?query={serverInfo{version}}', method: 'GET' }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(res.statusCode != 200 || body.toLowerCase().includes('error'));}); }).end();",
]
- CMD
- /nodejs/bin/node
- -e
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(res.statusCode != 200 || body.toLowerCase().includes('error'));}); }).end(); } catch { process.exit(1); }"
interval: 10s
timeout: 3s
retries: 30
timeout: 10s
retries: 3
start_period: 90s
ports:
- "0.0.0.0:3000:3000"
depends_on:
@@ -98,6 +96,7 @@ services:
S3_CREATE_BUCKET: "true"
FILE_SIZE_LIMIT_MB: 100
MAX_PROJECT_MODELS_PER_PAGE: 500
# TODO: Change this to a unique secret for this server
SESSION_SECRET: "TODO:ReplaceWithLongString"
@@ -111,47 +110,6 @@ services:
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
preview-service:
image: speckle/speckle-preview-service:latest
restart: always
depends_on:
speckle-server:
condition: service_healthy
mem_limit: "1000m"
memswap_limit: "1000m"
environment:
DEBUG: "preview-service:*"
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle"
webhook-service:
image: speckle/speckle-webhook-service:latest
restart: always
depends_on:
speckle-server:
condition: service_healthy
environment:
DEBUG: "webhook-service:*"
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle"
WAIT_HOSTS: postgres:5432
fileimport-service:
image: speckle/speckle-fileimport-service:latest
restart: always
depends_on:
speckle-server:
condition: service_healthy
environment:
DEBUG: "fileimport-service:*"
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle"
WAIT_HOSTS: postgres:5432
S3_ENDPOINT: "http://minio:9000"
S3_ACCESS_KEY: "minioadmin"
S3_SECRET_KEY: "minioadmin"
S3_BUCKET: "speckle-server"
SPECKLE_SERVER_URL: "http://speckle-server:3000"
networks:
default:
name: speckle-server
Generated
+1260 -987
View File
File diff suppressed because it is too large Load Diff
+13 -11
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>"]
@@ -15,28 +15,31 @@ packages = [
[tool.poetry.dependencies]
python = ">=3.8.0, <4.0"
pydantic = "^2.0"
python = ">=3.10.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"
pydantic-settings = "^2.6.1"
[tool.poetry.group.dev.dependencies]
black = "^22.8.0"
isort = "^5.7.0"
black = "24.10.0"
isort = "^5.13.2"
pytest = "^7.1.3"
pytest-asyncio = "^0.23.0"
pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
devtools = "^0.8.0"
pylint = "^2.14.4"
pylint = "^3.3.2"
pydantic-settings = "^2.3.0"
mypy = "^0.982"
pre-commit = "^2.20.0"
commitizen = "^2.38.0"
ruff = "^0.0.187"
commitizen = "^3.13.0"
ruff = "^0.8.2"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
@@ -58,8 +61,7 @@ exclude = '''
'''
include = '\.pyi?$'
line-length = 88
target-version = ["py37", "py38", "py39", "py310", "py311"]
target-version = ["py39", "py310", "py311", "py312", "py313"]
[tool.commitizen]
name = "cz_conventional_commits"
+3 -2
View File
@@ -1,4 +1,5 @@
"""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 (
@@ -6,8 +7,8 @@ from speckle_automate.schema import (
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResult,
ObjectResultLevel,
ResultCase,
)
__all__ = [
@@ -16,7 +17,7 @@ __all__ = [
"AutomationStatus",
"AutomationResult",
"AutomationRunData",
"ObjectResult",
"ResultCase",
"ObjectResultLevel",
"run_function",
"execute_automate_function",
+229 -80
View File
@@ -1,25 +1,28 @@
"""This module provides an abstraction layer above the Speckle Automate runtime."""
import time
from dataclasses import dataclass, field
from pathlib import Path
import time
from typing import Optional, Union
from typing import Any, Dict, List, Optional, Tuple, Union
import httpx
from gql import gql
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.objects import Base
from specklepy.transports.memory import MemoryTransport
from specklepy.transports.server import ServerTransport
from speckle_automate.schema import (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResult,
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
@@ -82,14 +85,21 @@ class AutomationContext:
"""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, self.automation_run_data.version_id
self.automation_run_data.project_id, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
@@ -97,14 +107,14 @@ class AutomationContext:
commit.referencedObject, self._server_transport, self._memory_transport
)
print(
f"It took {self.elapsed():2f} seconds to receive",
f" the speckle version {self.automation_run_data.version_id}",
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_id: str, version_message: str = ""
) -> None:
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:
@@ -112,11 +122,35 @@ class AutomationContext:
model_id (str): For now please use a `branchName`!
version_message (str): The message for the new version.
"""
if model_id == self.automation_run_data.model_id:
raise ValueError(
f"The target model id: {model_id} cannot match the model id"
f" that triggered this automation: {self.automation_run_data.model_id}"
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,
@@ -127,72 +161,91 @@ class AutomationContext:
version_id = self.speckle_client.commit.create(
stream_id=self.automation_run_data.project_id,
object_id=root_object_id,
branch_name=model_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 ReportFunctionRunStatus(
$automationId: String!,
$automationRevisionId: String!,
$automationRunId: String!,
$versionId: String!,
$functionId: String!,
$runStatus: AutomationRunStatus!
$elapsed: Float!
$resultVersionIds: [String!]!
mutation AutomateFunctionRunStatusReport(
$projectId: String!
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
$objectResults: JSONObject
$results: JSONObject
$contextView: String
){
automationMutations {
functionRunStatusReport(input: {
automationId: $automationId
automationRevisionId: $automationRevisionId
automationRunId: $automationRunId
versionId: $versionId
functionRuns: [
{
functionId: $functionId
status: $runStatus,
elapsed: $elapsed,
resultVersionIds: $resultVersionIds,
statusMessage: $statusMessage
results: $objectResults
}]
})
}
automateFunctionRunStatusReport(input: {
projectId: $projectId
functionRunId: $functionRunId
status: $status
statusMessage: $statusMessage
contextView: $contextView
results: $results
})
}
"""
"""
)
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
object_results = {
"version": "1.0.0",
"version": 1,
"values": {
"speckleObjects": self._automation_result.model_dump(by_alias=True)[
"objectResults": self._automation_result.model_dump(by_alias=True)[
"objectResults"
],
"blobs": self._automation_result.blobs,
"blobIds": self._automation_result.blobs,
},
}
else:
object_results = None
params = {
"automationId": self.automation_run_data.automation_id,
"automationRevisionId": self.automation_run_data.automation_revision_id,
"automationRunId": self.automation_run_data.automation_run_id,
"versionId": self.automation_run_data.version_id,
"functionId": self.automation_run_data.function_id,
"runStatus": self.run_status.value,
"projectId": self.automation_run_data.project_id,
"functionRunId": self.automation_run_data.function_run_id,
"status": self.run_status.value,
"statusMessage": self._automation_result.status_message,
"elapsed": self.elapsed(),
"resultVersionIds": self._automation_result.result_versions,
"objectResults": object_results,
"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:
@@ -214,7 +267,7 @@ class AutomationContext:
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.speckle_server_url}api/stream/"
f"{self.automation_run_data.project_id}/blob"
)
data = (
@@ -232,13 +285,18 @@ class AutomationContext:
if len(upload_response.upload_results) != 1:
raise ValueError("Expecting one upload result.")
for upload_result in upload_response.upload_results:
self._automation_result.blobs.append(upload_result.blob_id)
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_exception(self, status_message: str) -> None:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.EXCEPTION, 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)
@@ -251,28 +309,119 @@ class AutomationContext:
self._automation_result.run_status = status
self._automation_result.elapsed = duration
msg = f"Automation run {status.value} after {duration:2f} seconds."
msg = f"Automation run {status.value} after {duration:.2f} seconds."
print("\n".join([msg, status_message]) if status_message else msg)
def add_object_error(self, object_id: str, error_cause: str) -> None:
"""Add an error to a given Speckle object."""
self._add_object_result(object_id, ObjectResultLevel.ERROR, error_cause)
def add_object_warning(self, object_id: str, warning: str) -> None:
"""Add a warning to a given Speckle object."""
self._add_object_result(object_id, ObjectResultLevel.WARNING, warning)
def add_object_info(self, object_id: str, info: str) -> None:
"""Add an info message to a given Speckle object."""
self._add_object_result(object_id, ObjectResultLevel.INFO, info)
def _add_object_result(
self, object_id: str, level: ObjectResultLevel, status_message: str
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_success_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 success case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.SUCCESS,
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"Object {object_id} was marked with {level.value.upper()}",
f" cause: {status_message}",
f"Created new {level.value.upper()}"
f" category: {category} caused by: {message}"
)
self._automation_result.object_results[object_id].append(
ObjectResult(level=level, status_message=status_message)
self._automation_result.object_results.append(
ResultCase(
category=category,
level=level,
object_ids=id_list,
message=message,
metadata=metadata,
visual_overrides=visual_overrides,
)
)
+155
View File
@@ -0,0 +1,155 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
import pytest
from gql import gql
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from speckle_automate.schema import AutomationRunData, TestAutomationRunData
from specklepy.api.client import SpeckleClient
class TestAutomationEnvironment(BaseSettings):
"""Get known environment variables from local `.env` file"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="speckle_",
extra="ignore",
)
token: str = Field()
server_url: str = Field()
project_id: str = Field()
automation_id: str = Field()
@pytest.fixture()
def test_automation_environment() -> TestAutomationEnvironment:
return TestAutomationEnvironment()
@pytest.fixture()
def test_automation_token(
test_automation_environment: TestAutomationEnvironment,
) -> str:
"""Provide a speckle token for the test suite."""
return test_automation_environment.token
@pytest.fixture()
def speckle_client(
test_automation_environment: TestAutomationEnvironment,
) -> SpeckleClient:
"""Initialize a SpeckleClient for testing."""
speckle_client = SpeckleClient(
test_automation_environment.server_url,
test_automation_environment.server_url.startswith("https"),
)
speckle_client.authenticate_with_token(test_automation_environment.token)
return speckle_client
def create_test_automation_run(
speckle_client: SpeckleClient, project_id: str, test_automation_id: str
) -> TestAutomationRunData:
"""Create test run to report local test results to"""
query = gql(
"""
mutation CreateTestRun(
$projectId: ID!,
$automationId: ID!
) {
projectMutations {
automationMutations(projectId: $projectId) {
createTestAutomationRun(automationId: $automationId) {
automationRunId
functionRunId
triggers {
payload {
modelId
versionId
}
triggerType
}
}
}
}
}
"""
)
params = {"automationId": test_automation_id, "projectId": project_id}
result = speckle_client.httpclient.execute(query, params)
print(result)
return (
result.get("projectMutations")
.get("automationMutations")
.get("createTestAutomationRun")
)
@pytest.fixture()
def test_automation_run(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> TestAutomationRunData:
return create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
def create_test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
"""Create automation run data for a new run for a given test automation"""
test_automation_run_data = create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
return AutomationRunData(
project_id=test_automation_environment.project_id,
speckle_server_url=test_automation_environment.server_url,
automation_id=test_automation_environment.automation_id,
automation_run_id=test_automation_run_data["automationRunId"],
function_run_id=test_automation_run_data["functionRunId"],
triggers=test_automation_run_data["triggers"],
)
@pytest.fixture()
def test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
return create_test_automation_run_data(speckle_client, test_automation_environment)
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()
__all__ = [
"test_automation_environment",
"test_automation_token",
"speckle_client",
"test_automation_run",
"test_automation_run_data",
]
+90 -52
View File
@@ -3,12 +3,15 @@
Provides mechanisms to execute any function,
that conforms to the AutomateFunction "interface"
"""
import json
import os
import sys
import traceback
from pathlib import Path
from typing import Callable, Optional, TypeVar, Union, overload
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
@@ -19,17 +22,59 @@ 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:
...
) -> None: ...
@overload
def execute_automate_function(automate_function: AutomateFunctionWithoutInputs) -> None:
...
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(
@@ -40,55 +85,53 @@ def execute_automate_function(
# first arg is the python file name, we do not need that
args = sys.argv[1:]
if len(args) < 2:
raise ValueError("too few arguments specified need minimum 2")
if len(args) > 4:
raise ValueError("too many arguments specified, max supported is 4")
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 = args[0]
command, argument = args
if command == "generate_schema":
path = Path(args[1])
path = Path(argument)
schema = json.dumps(
input_schema.model_json_schema(by_alias=True) if input_schema else {}
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 = args[1]
function_inputs = args[2]
speckle_token = os.environ.get("SPECKLE_TOKEN", None)
if not speckle_token and len(args) != 4:
raise ValueError("Cannot get speckle token from arguments or environment")
speckle_token = speckle_token if speckle_token else args[3]
inputs = (
input_schema.model_validate_json(function_inputs)
if input_schema
else input_schema
automation_run_data, function_inputs, speckle_token = _parse_input_data(
argument, input_schema
)
if inputs:
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
if function_inputs:
automation_context = run_function(
automation_context,
automate_function, # type: ignore
automation_run_data,
speckle_token,
inputs,
)
else:
automation_context = run_function(
automate_function, # type: ignore
automation_run_data,
speckle_token,
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_code = (
0 if automation_context.run_status == AutomationStatus.SUCCEEDED else 1
1 if automation_context.run_status == AutomationStatus.EXCEPTION else 0
)
exit(exit_code)
@@ -98,33 +141,25 @@ def execute_automate_function(
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunction[T],
automation_run_data: Union[AutomationRunData, str],
speckle_token: str,
inputs: T,
) -> AutomationContext:
...
) -> AutomationContext: ...
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunctionWithoutInputs,
automation_run_data: Union[AutomationRunData, str],
speckle_token: str,
) -> AutomationContext:
...
) -> AutomationContext: ...
def run_function(
automation_context: AutomationContext,
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
automation_run_data: Union[AutomationRunData, str],
speckle_token: str,
inputs: Optional[T] = None,
) -> AutomationContext:
"""Run the provided function with the automate sdk context."""
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
automation_context.report_run_status()
try:
@@ -139,6 +174,7 @@ def run_function(
if automation_context.run_status not in [
AutomationStatus.FAILED,
AutomationStatus.SUCCEEDED,
AutomationStatus.EXCEPTION,
]:
automation_context.mark_run_success(
"WARNING: Automate assumed a success status,"
@@ -147,9 +183,11 @@ def run_function(
except Exception:
trace = traceback.format_exc()
print(trace)
automation_context.mark_run_failed(
automation_context.mark_run_exception(
"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
+41 -16
View File
@@ -1,7 +1,7 @@
""""""
from collections import defaultdict
from enum import Enum
from typing import Optional, List, Dict
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from stringcase import camelcase
@@ -13,21 +13,43 @@ class AutomateBase(BaseModel):
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
model_id: str
branch_name: str
version_id: str
speckle_server_url: str
automation_id: str
automation_revision_id: str
automation_run_id: str
function_run_id: str
function_id: str
function_release: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
class TestAutomationRunData(BaseModel):
"""Values of the run created in the test automation for local test results."""
automation_run_id: str
function_run_id: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
@@ -41,21 +63,27 @@ class AutomationStatus(str, Enum):
RUNNING = "RUNNING"
FAILED = "FAILED"
SUCCEEDED = "SUCCEEDED"
EXCEPTION = "EXCEPTION"
class ObjectResultLevel(str, Enum):
"""Possible status message levels for object reports."""
SUCCESS = "SUCCESS"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
class ObjectResult(AutomateBase):
"""An object level result."""
class ResultCase(AutomateBase):
"""A result case."""
category: str
level: ObjectResultLevel
status_message: str
object_ids: List[str]
message: Optional[str]
metadata: Optional[Dict[str, Any]]
visual_overrides: Optional[Dict[str, Any]]
class AutomationResult(AutomateBase):
@@ -67,7 +95,4 @@ class AutomationResult(AutomateBase):
blobs: List[str] = Field(default_factory=list)
run_status: AutomationStatus = AutomationStatus.RUNNING
status_message: Optional[str] = None
object_results: Dict[str, List[ObjectResult]] = Field(
default_factory=lambda: defaultdict(list) # typing: ignore
)
object_results: list[ResultCase] = Field(default_factory=list)
+3
View File
@@ -0,0 +1,3 @@
# from specklepy import objects
# __all__ = ["objects"]
+76 -37
View File
@@ -1,30 +1,24 @@
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,
ActiveUserResource,
ModelResource,
OtherUserResource,
ProjectInviteResource,
ProjectResource,
ServerResource,
SubscriptionResource,
VersionResource,
branch,
commit,
object,
other_user,
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):
@@ -32,7 +26,7 @@ class SpeckleClient(CoreSpeckleClient):
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `speckle.xyz`.
or you can use our public server `app.speckle.systems`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
@@ -43,7 +37,7 @@ class SpeckleClient(CoreSpeckleClient):
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
@@ -58,43 +52,82 @@ class SpeckleClient(CoreSpeckleClient):
```
"""
DEFAULT_HOST = "speckle.xyz"
DEFAULT_HOST = "app.speckle.systems"
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()
def _init_resources(self) -> None:
self.server = server.Resource(
self.server = ServerResource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
try:
server_version = self.server.version()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = ActiveUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project = ProjectResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project_invite = ProjectInviteResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.model = ModelResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.version = VersionResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.subscription = SubscriptionResource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
# todo: why doesn't this take a server version
)
# Deprecated Resources
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.other_user = other_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = active_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
@@ -131,7 +164,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 +178,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 +192,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",
]
-12
View File
@@ -1,12 +0,0 @@
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
# 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)
+35
View File
@@ -0,0 +1,35 @@
# 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 (
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,
)
+40 -8
View File
@@ -1,9 +1,41 @@
import pkgutil
import sys
from importlib import import_module
from specklepy.api.resources.current.active_user_resource import ActiveUserResource
from specklepy.api.resources.current.model_resource import ModelResource
from specklepy.api.resources.current.other_user_resource import OtherUserResource
from specklepy.api.resources.current.project_invite_resource import (
ProjectInviteResource,
)
from specklepy.api.resources.current.project_resource import ProjectResource
from specklepy.api.resources.current.server_resource import ServerResource
from specklepy.api.resources.current.subscription_resource import SubscriptionResource
from specklepy.api.resources.current.version_resource import VersionResource
from specklepy.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
for _, name, _ in pkgutil.iter_modules(__path__):
imported_module = import_module("." + name, package=__name__)
if hasattr(imported_module, "Resource"):
setattr(sys.modules[__name__], name, imported_module)
__all__ = [
"ActiveUserResource",
"ModelResource",
"OtherUserResource",
"ProjectInviteResource",
"ProjectResource",
"ServerResource",
"SubscriptionResource",
"VersionResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
-120
View File
@@ -1,120 +0,0 @@
from datetime import datetime, timezone
from typing import List, Optional
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.active_user import Resource as CoreResource
class Resource(CoreResource):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = User
def get(self) -> User:
"""Gets the profile of a user. If no id argument is provided,
will return the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
metrics.track(metrics.SDK, custom_props={"name": "User Active Get"})
return super().get()
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Update"})
return super().update(name, company, bio, avatar)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's
activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
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
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
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.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
@@ -0,0 +1,144 @@
from datetime import datetime
from typing import List, Optional, overload
from deprecated import deprecated
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.models import (
PendingStreamCollaborator,
Project,
ResourceCollection,
User,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import ActiveUserResource as CoreResource
from specklepy.logging import metrics
class ActiveUserResource(CoreResource):
"""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__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = User
def get(self) -> Optional[User]:
metrics.track(metrics.SDK, self.account, {"name": "Active User Get"})
return super().get()
@deprecated("Use UserUpdateInput overload", version=FE1_DEPRECATION_VERSION)
@overload
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
) -> User: ...
@overload
def update(self, *, input: UserUpdateInput) -> User: ...
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
*,
input: Optional[UserUpdateInput] = None,
) -> User:
metrics.track(metrics.SDK, self.account, {"name": "Active User Update"})
if isinstance(input, UserUpdateInput):
return super()._update(input=input)
else:
return super()._update(
input=UserUpdateInput(
name=name,
company=company,
bio=bio,
avatar=avatar,
)
)
def get_projects(
self,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[UserProjectsFilter] = None,
) -> ResourceCollection[Project]:
metrics.track(metrics.SDK, self.account, {"name": "Active User Get Projects"})
return super().get_projects(limit=limit, cursor=cursor, filter=filter)
def get_project_invites(self) -> List[PendingStreamCollaborator]:
metrics.track(
metrics.SDK, self.account, {"name": "Active User Get Project Invites"}
)
return super().get_project_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
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
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)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Fetches all of the current user's pending stream invitations.
Returns:
List[PendingStreamCollaborator]: A list of pending stream invitations.
"""
metrics.track(
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
)
return super().get_all_pending_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Fetches a specific pending invite for the current user on a given stream.
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:
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)
@@ -0,0 +1,74 @@
from typing import Optional
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
ModelVersionsFilter,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
from specklepy.core.api.resources import ModelResource as CoreResource
from specklepy.logging import metrics
class ModelResource(CoreResource):
"""API Access class for models"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, model_id: str, project_id: str) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Get"})
return super().get(model_id, project_id)
def get_with_versions(
self,
model_id: str,
project_id: str,
*,
versions_limit: int = 25,
versions_cursor: Optional[str] = None,
versions_filter: Optional[ModelVersionsFilter] = None,
) -> ModelWithVersions:
metrics.track(metrics.SDK, self.account, {"name": "Model Get With Versions"})
return super().get_with_versions(
model_id,
project_id,
versions_limit=versions_limit,
versions_cursor=versions_cursor,
versions_filter=versions_filter,
)
def get_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ResourceCollection[Model]:
metrics.track(metrics.SDK, self.account, {"name": "Model Get Models"})
return super().get_models(
project_id,
models_limit=models_limit,
models_cursor=models_cursor,
models_filter=models_filter,
)
def create(self, input: CreateModelInput) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Create"})
return super().create(input)
def delete(self, input: DeleteModelInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Model Delete"})
return super().delete(input)
def update(self, input: UpdateModelInput) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Update"})
return super().update(input)
@@ -0,0 +1,104 @@
from datetime import datetime
from typing import List, Optional, Union
from deprecated import deprecated
from specklepy.core.api.models import (
ActivityCollection,
LimitedUser,
UserSearchResultCollection,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import OtherUserResource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class OtherUserResource(CoreResource):
"""
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__(
account=account,
basepath=basepath,
client=client,
server_version=(server_version,),
)
self.schema = LimitedUser
def get(self, id: str) -> Optional[LimitedUser]:
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
def user_search(
self,
query: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
archived: bool = False,
emailOnly: bool = False,
) -> UserSearchResultCollection:
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().user_search(
query, limit=limit, cursor=cursor, archived=archived, emailOnly=emailOnly
)
@deprecated(reason="Use user_search instead", version=FE1_DEPRECATION_VERSION)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""
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.
Returns:
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."
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
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.
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)
@@ -0,0 +1,54 @@
from typing import Any, Optional, Tuple
from gql import Client
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import (
ProjectInviteCreateInput,
ProjectInviteUseInput,
)
from specklepy.core.api.models import PendingStreamCollaborator, ProjectWithTeam
from specklepy.core.api.resources import ProjectInviteResource as CoreResource
from specklepy.logging import metrics
class ProjectInviteResource(CoreResource):
"""API Access class for project invites"""
def __init__(
self,
account: Account,
basepath: str,
client: Client,
server_version: Optional[Tuple[Any, ...]],
) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def create(
self, project_id: str, input: ProjectInviteCreateInput
) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Create"})
return super().create(project_id, input)
def use(self, input: ProjectInviteUseInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Use"})
return super().use(input)
def get(
self, project_id: str, token: Optional[str]
) -> Optional[PendingStreamCollaborator]:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Get"})
return super().get(project_id, token)
def cancel(
self,
project_id: str,
invite_id: str,
) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Cancel"})
return super().cancel(project_id, invite_id)
@@ -0,0 +1,63 @@
from typing import Optional
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
from specklepy.core.api.resources import ProjectResource as CoreResource
from specklepy.logging import metrics
class ProjectResource(CoreResource):
"""API Access class for projects"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, project_id: str) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Get "})
return super().get(project_id)
def get_with_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ProjectWithModels:
metrics.track(metrics.SDK, self.account, {"name": "Project Get With Models"})
return super().get_with_models(
project_id,
models_limit=models_limit,
models_cursor=models_cursor,
models_filter=models_filter,
)
def get_with_team(self, project_id: str) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Get With Team"})
return super().get_with_team(project_id)
def create(self, input: ProjectCreateInput) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Create"})
return super().create(input)
def update(self, input: ProjectUpdateInput) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Update"})
return super().update(input)
def delete(self, project_id: str) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Project Delete"})
return super().delete(project_id)
def update_role(self, input: ProjectUpdateRoleInput) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Update Role"})
return super().update_role(input)
@@ -1,17 +1,11 @@
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.core.api.resources import ServerResource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import GraphQLException
from specklepy.core.api.resources.server import Resource as CoreResource
class Resource(CoreResource):
class ServerResource(CoreResource):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
@@ -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)
@@ -0,0 +1,64 @@
from typing import Callable, Optional, Sequence
from pydantic import BaseModel
from typing_extensions import TypeVar
from specklepy.core.api.models import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
)
from specklepy.core.api.resources import SubscriptionResource as CoreResource
from specklepy.logging import metrics
TEventArgs = TypeVar("TEventArgs", bound=BaseModel)
class SubscriptionResource(CoreResource):
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
async def user_projects_updated(
self, callback: Callable[[UserProjectsUpdatedMessage], None]
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Models Updated"}
)
return await super().user_projects_updated(callback)
async def project_models_updated(
self,
callback: Callable[[ProjectModelsUpdatedMessage], None],
id: str,
*,
model_ids: Optional[Sequence[str]] = None,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Models Updated"}
)
return await super().project_models_updated(callback, id, model_ids=model_ids)
async def project_updated(
self,
callback: Callable[[ProjectUpdatedMessage], None],
id: str,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Updated"}
)
return await super().project_updated(callback, id)
async def project_versions_updated(
self,
callback: Callable[[ProjectVersionsUpdatedMessage], None],
id: str,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Versions Updated"}
)
return await super().project_versions_updated(callback, id)
@@ -0,0 +1,63 @@
from typing import Optional
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.inputs.version_inputs import (
CreateVersionInput,
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
from specklepy.core.api.models import ResourceCollection, Version
from specklepy.core.api.resources import VersionResource as CoreResource
from specklepy.logging import metrics
class VersionResource(CoreResource):
"""API Access class for model versions"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, version_id: str, project_id: str) -> Version:
metrics.track(metrics.SDK, self.account, {"name": "Version Get"})
return super().get(version_id, project_id)
def get_versions(
self,
model_id: str,
project_id: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[ModelVersionsFilter] = None,
) -> ResourceCollection[Version]:
metrics.track(metrics.SDK, self.account, {"name": "Version Get Versions"})
return super().get_versions(
model_id, project_id, limit=limit, cursor=cursor, filter=filter
)
def create(self, input: CreateVersionInput) -> str:
metrics.track(metrics.SDK, self.account, {"name": "Version Create"})
return super().create(input)
def update(self, input: UpdateVersionInput) -> Version:
metrics.track(metrics.SDK, self.account, {"name": "Version Update"})
return super().update(input)
def move_to_model(self, input: MoveVersionsInput) -> str:
metrics.track(metrics.SDK, self.account, {"name": "Version Move To Model"})
return super().move_to_model(input)
def delete(self, input: DeleteVersionsInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Version Delete"})
return super().delete(input)
def received(self, input: MarkReceivedVersionInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Version Received"})
return super().received(input)
@@ -0,0 +1,9 @@
from deprecated import deprecated
from specklepy.api.resources import ActiveUserResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(ActiveUserResource):
"""Renamed to ActiveUserResource"""
@@ -1,12 +1,15 @@
from typing import Optional
from typing import Optional, Union
from gql import gql
from deprecated import deprecated
from specklepy.api.models import Branch
from specklepy.api.resource import ResourceBase
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.branch import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.core.api.resources.branch import Resource as CoreResource
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
@@ -20,6 +23,7 @@ class Resource(CoreResource):
)
self.schema = Branch
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self, stream_id: str, name: str, description: str = "No description provided"
) -> str:
@@ -35,7 +39,10 @@ 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):
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
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:
@@ -49,6 +56,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch Get"})
return super().get(stream_id, name, commits_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
@@ -63,6 +71,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch List"})
return super().list(stream_id, branches_limit, commits_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
stream_id: str,
@@ -84,6 +93,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch Update"})
return super().update(stream_id, branch_id, name, description)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
@@ -1,12 +1,15 @@
from typing import List, Optional
from typing import List, Optional, Union
from gql import gql
from deprecated import deprecated
from specklepy.api.models import Commit
from specklepy.api.resource import ResourceBase
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.commit import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.core.api.resources.commit import Resource as CoreResource
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
@@ -20,6 +23,7 @@ class Resource(CoreResource):
)
self.schema = Commit
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, commit_id: str) -> Commit:
"""
Gets a commit given a stream and the commit id
@@ -34,6 +38,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Commit Get"})
return super().get(stream_id, commit_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, limit: int = 10) -> List[Commit]:
"""
Get a list of commits on a given stream
@@ -48,6 +53,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Commit List"})
return super().list(stream_id, limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
stream_id: str,
@@ -55,8 +61,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,8 +82,11 @@ 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
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
Update a commit
@@ -94,6 +103,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Commit Update"})
return super().update(stream_id, commit_id, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
Delete a commit
@@ -109,6 +119,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Commit Delete"})
return super().delete(stream_id, commit_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def received(
self,
stream_id: str,
@@ -1,13 +1,14 @@
from typing import Dict, List
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.objects.base import Base
from deprecated import deprecated
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.object import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.core.api.resources.object import Resource as CoreResource
from specklepy.objects.base import Base
class Resource(CoreResource):
@@ -21,6 +22,7 @@ class Resource(CoreResource):
)
self.schema = Base
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, object_id: str) -> Base:
"""
Get a stream object
@@ -35,6 +37,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Object Get"})
return super().get(stream_id, object_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(self, stream_id: str, objects: List[Dict]) -> str:
"""
Not advised - generally, you want to use `operations.send()`.
@@ -58,4 +61,3 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "Object Create"})
return super().create(stream_id, objects)
@@ -0,0 +1,11 @@
from deprecated import deprecated
from specklepy.api.resources import OtherUserResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to OtherUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(OtherUserResource):
"""
Renamed to OtherUserResource
"""
@@ -0,0 +1,9 @@
from deprecated import deprecated
from specklepy.api.resources import ServerResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ServerResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""Renamed to ServerResource"""
@@ -1,15 +1,15 @@
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.api.models import PendingStreamCollaborator, Stream
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.stream import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
from specklepy.core.api.resources.stream import Resource as CoreResource
class Resource(CoreResource):
@@ -25,6 +25,7 @@ class Resource(CoreResource):
self.schema = Stream
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream:
"""Get the specified stream from the server
@@ -39,6 +40,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Get"})
return super().get(id, branch_limit, commit_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_limit: int = 10) -> List[Stream]:
"""Get a list of the user's streams
@@ -51,6 +53,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream List"})
return super().list(stream_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
name: str = "Anonymous Python Stream",
@@ -71,6 +74,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Create"})
return super().create(name, description, is_public)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
id: str,
@@ -93,6 +97,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Update"})
return super().update(id, name, description, is_public)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, id: str) -> bool:
"""Delete a stream given its id
@@ -105,6 +110,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Delete"})
return super().delete(id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def search(
self,
search_query: str,
@@ -126,6 +132,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Search"})
return super().search(search_query, limit, branch_limit, commit_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream.
@@ -140,6 +147,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Favorite"})
return super().favorite(stream_id, favorited)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(
self, stream_id: str
) -> List[PendingStreamCollaborator]:
@@ -158,6 +166,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Get"})
return super().get_all_pending_invites(stream_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite(
self,
stream_id: str,
@@ -185,6 +194,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Create"})
return super().invite(stream_id, email, user_id, role, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_batch(
self,
stream_id: str,
@@ -211,6 +221,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Batch Create"})
return super().invite_batch(stream_id, emails, user_ids, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite
@@ -226,6 +237,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Cancel"})
return super().invite_cancel(stream_id, invite_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite
@@ -243,6 +255,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Invite Use"})
return super().invite_use(stream_id, token, accept)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream
@@ -256,9 +269,14 @@ 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)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
@@ -272,6 +290,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Permission Revoke"})
return super().revoke_permission(stream_id, user_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
stream_id: str,
@@ -301,4 +320,3 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Activity"})
return super().activity(stream_id, action_type, limit, before, after, cursor)
@@ -1,27 +1,17 @@
from functools import wraps
from typing import Callable, Dict, List, Optional, Union
from gql import gql
from deprecated import deprecated
from graphql import DocumentNode
from specklepy.api.resource import ResourceBase
from specklepy.api.resources.stream import Stream
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.current.subscription_resource import check_wsclient
from specklepy.core.api.resources.deprecated.subscriptions import (
Resource as CoreResource,
)
from specklepy.logging import metrics
from specklepy.core.api.resources.subscriptions import Resource as CoreResource
def check_wsclient(function):
@wraps(function)
async def check_wsclient_wrapper(self, *args, **kwargs):
if self.client is None:
raise SpeckleException(
"You must authenticate before you can subscribe to events"
)
else:
return await function(self, *args, **kwargs)
return check_wsclient_wrapper
class Resource(CoreResource):
@@ -34,6 +24,7 @@ class Resource(CoreResource):
client=client,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_added(self, callback: Optional[Callable] = None):
"""Subscribes to new stream added event for your profile.
@@ -49,6 +40,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Added"})
return super().stream_added(callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_updated(self, id: str, callback: Optional[Callable] = None):
"""
@@ -64,9 +56,12 @@ 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)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_removed(self, callback: Optional[Callable] = None):
"""Subscribes to stream removed event for your profile.
@@ -83,9 +78,12 @@ 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)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def subscribe(
self,
@@ -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.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.deprecated.user import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.core.api.resources.user import Resource as CoreResource
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)
-87
View File
@@ -1,87 +0,0 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from gql import gql
from specklepy.api.models import ActivityCollection, LimitedUser
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from 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."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = LimitedUser
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
Arguments:
id {str} -- the user id
Returns:
LimitedUser -- the retrieved profile of another user
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""Searches for user by name or email. The search query must be at least
3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[LimitedUser] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
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
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
+8 -14
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):
"""
@@ -30,7 +22,7 @@ class StreamWrapper(CoreStreamWrapper):
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
@@ -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)
+88 -41
View File
@@ -11,15 +11,20 @@ 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,
ActiveUserResource,
ModelResource,
OtherUserResource,
ProjectInviteResource,
ProjectResource,
ServerResource,
SubscriptionResource,
VersionResource,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
@@ -30,7 +35,7 @@ class SpeckleClient:
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `speckle.xyz`.
or you can use our public server `app.speckle.systems`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
@@ -41,7 +46,7 @@ class SpeckleClient:
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
@@ -56,10 +61,17 @@ class SpeckleClient:
```
"""
DEFAULT_HOST = "speckle.xyz"
DEFAULT_HOST = "app.speckle.systems"
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 +86,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 +135,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 +146,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 +169,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,
@@ -162,53 +181,81 @@ class SpeckleClient:
self._init_resources()
try:
user_or_error = self.active_user.get()
if isinstance(user_or_error, SpeckleException):
if isinstance(user_or_error.exception, TransportServerError):
raise user_or_error.exception
else:
raise user_or_error
except TransportServerError as ex:
if ex.code == 403:
warn(
SpeckleWarning(
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
_ = self.active_user.get()
except SpeckleException as ex:
if isinstance(ex.exception, TransportServerError):
if ex.exception.code == 403:
warn(
SpeckleWarning(
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
)
)
)
else:
raise ex
else:
raise ex
def execute_query(self, query: str) -> Dict:
return self.httpclient.execute(query)
def _init_resources(self) -> None:
self.server = server.Resource(
self.server = ServerResource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
try:
server_version = self.server.version()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = ActiveUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project = ProjectResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project_invite = ProjectInviteResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.model = ModelResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.version = VersionResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.subscription = SubscriptionResource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
)
# Deprecated Resources
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.other_user = other_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = active_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
+26 -3
View File
@@ -1,21 +1,22 @@
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
class UserInfo(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
email: Optional[str] = None
company: Optional[str] = None
id: Optional[str] = None
avatar: Optional[str] = None
class Account(BaseModel):
@@ -110,7 +111,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 +144,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(
+29
View File
@@ -0,0 +1,29 @@
from enum import Enum
class ProjectVisibility(str, Enum):
PRIVATE = "PRIVATE"
PUBLIC = "PUBLIC"
UNLISTEd = "UNLISTED"
class UserProjectsUpdatedMessageType(str, Enum):
ADDED = "ADDED"
REMOVED = "REMOVED"
class ProjectModelsUpdatedMessageType(str, Enum):
CREATED = "CREATED"
DELETED = "DELETED"
UPDATED = "UPDATED"
class ProjectUpdatedMessageType(str, Enum):
DELETED = "DELETED"
UPDATED = "UPDATED"
class ProjectVersionsUpdatedMessageType(str, Enum):
CREATED = "CREATED"
DELETED = "DELETED"
UPDATED = "UPDATED"
+42
View File
@@ -0,0 +1,42 @@
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
ModelVersionsFilter,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectInviteCreateInput,
ProjectInviteUseInput,
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.inputs.version_inputs import (
CreateVersionInput,
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
__all__ = [
"CreateModelInput",
"DeleteModelInput",
"UpdateModelInput",
"ModelVersionsFilter",
"ProjectCreateInput",
"ProjectInviteCreateInput",
"ProjectInviteUseInput",
"ProjectModelsFilter",
"ProjectUpdateInput",
"ProjectUpdateRoleInput",
"UserProjectsFilter",
"UserUpdateInput",
"UpdateVersionInput",
"MoveVersionsInput",
"DeleteVersionsInput",
"CreateVersionInput",
"MarkReceivedVersionInput",
]
@@ -0,0 +1,26 @@
from typing import Optional, Sequence
from pydantic import BaseModel
class CreateModelInput(BaseModel):
name: str
description: Optional[str] = None
projectId: str
class DeleteModelInput(BaseModel):
id: str
projectId: str
class UpdateModelInput(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
projectId: str
class ModelVersionsFilter(BaseModel):
priorityIds: Sequence[str]
priorityIdsOnly: Optional[bool] = None
@@ -0,0 +1,47 @@
from typing import Optional, Sequence
from pydantic import BaseModel
from specklepy.core.api.enums import ProjectVisibility
class ProjectCreateInput(BaseModel):
name: Optional[str]
description: Optional[str]
visibility: Optional[ProjectVisibility]
class ProjectInviteCreateInput(BaseModel):
email: Optional[str]
role: Optional[str]
serverRole: Optional[str]
userId: Optional[str]
class ProjectInviteUseInput(BaseModel):
accept: bool
projectId: str
token: str
class ProjectModelsFilter(BaseModel):
contributors: Optional[Sequence[str]] = None
excludeIds: Optional[Sequence[str]] = None
ids: Optional[Sequence[str]] = None
onlyWithVersions: Optional[bool] = None
search: Optional[str] = None
sourceApps: Optional[Sequence[str]] = None
class ProjectUpdateInput(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
allowPublicComments: Optional[bool] = None
visibility: Optional[ProjectVisibility] = None
class ProjectUpdateRoleInput(BaseModel):
userId: str
projectId: str
role: Optional[str]
@@ -0,0 +1,15 @@
from typing import Optional, Sequence
from pydantic import BaseModel
class UserUpdateInput(BaseModel):
avatar: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
name: Optional[str] = None
class UserProjectsFilter(BaseModel):
search: str
onlyWithRoles: Optional[Sequence[str]] = None
@@ -0,0 +1,37 @@
from typing import Optional, Sequence
from pydantic import BaseModel
class UpdateVersionInput(BaseModel):
versionId: str
projectId: str
message: Optional[str]
class MoveVersionsInput(BaseModel):
targetModelName: str
versionIds: Sequence[str]
projectId: str
class DeleteVersionsInput(BaseModel):
versionIds: Sequence[str]
projectId: str
class CreateVersionInput(BaseModel):
objectId: str
modelId: str
projectId: str
message: Optional[str] = None
sourceApplication: Optional[str] = "py"
totalChildrenCount: Optional[int] = None
parents: Optional[Sequence[str]] = None
class MarkReceivedVersionInput(BaseModel):
versionId: str
projectId: str
sourceApplication: str
message: Optional[str] = None
+71
View File
@@ -0,0 +1,71 @@
from specklepy.core.api.models.current import (
AuthStrategy,
LimitedUser,
Model,
ModelWithVersions,
PendingStreamCollaborator,
Project,
ProjectCollaborator,
ProjectCommentCollection,
ProjectWithModels,
ProjectWithTeam,
ResourceCollection,
ServerConfiguration,
ServerInfo,
ServerMigration,
User,
UserSearchResultCollection,
Version,
)
from specklepy.core.api.models.deprecated import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
Object,
Stream,
Streams,
)
from specklepy.core.api.models.subscription_messages import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
)
__all__ = [
"User",
"ResourceCollection",
"ServerMigration",
"AuthStrategy",
"ServerConfiguration",
"ServerInfo",
"LimitedUser",
"PendingStreamCollaborator",
"ProjectCollaborator",
"Version",
"Model",
"ModelWithVersions",
"Project",
"ProjectWithModels",
"ProjectWithTeam",
"ProjectCommentCollection",
"UserSearchResultCollection",
"UserProjectsUpdatedMessage",
"ProjectModelsUpdatedMessage",
"ProjectUpdatedMessage",
"ProjectVersionsUpdatedMessage",
"Collaborator",
"Commit",
"Commits",
"Object",
"Branch",
"Branches",
"Stream",
"Streams",
"Activity",
"ActivityCollection",
]
+171
View File
@@ -0,0 +1,171 @@
from datetime import datetime
from typing import Generic, List, Optional, TypeVar
from pydantic import BaseModel
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.models.deprecated import Streams
T = TypeVar("T")
class User(BaseModel):
id: str
email: Optional[str] = None
name: str
bio: Optional[str] = None
company: Optional[str] = None
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
streams: Optional["Streams"] = None
def __repr__(self):
return (
f"User( id: {self.id}, name: {self.name}, email: {self.email}, company:"
f" {self.company} )"
)
def __str__(self) -> str:
return self.__repr__()
class ResourceCollection(BaseModel, Generic[T]):
totalCount: int
items: List[T]
cursor: Optional[str] = None
class ServerMigration(BaseModel):
movedFrom: Optional[str]
movedTo: Optional[str]
class AuthStrategy(BaseModel):
color: Optional[str]
icon: str
id: str
name: str
url: str
class ServerConfiguration(BaseModel):
blobSizeLimitBytes: int
objectMultipartUploadSizeLimitBytes: int
objectSizeLimitBytes: int
# Keeping this one all Optionals at the minute, because its used both as a deserialization model for GQL and Account Management
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
url: Optional[str] = None
adminContact: Optional[str] = None
description: Optional[str] = None
canonicalUrl: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
# TODO separate gql model from account management model
class LimitedUser(BaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
name: str
bio: Optional[str]
company: Optional[str]
avatar: Optional[str]
verified: Optional[bool]
role: Optional[str]
class PendingStreamCollaborator(BaseModel):
id: str
inviteId: str
streamId: Optional[str] = None
projectId: str
streamName: Optional[str] = None
projectName: str
title: str
role: str
invitedBy: LimitedUser
user: Optional[LimitedUser] = None
token: Optional[str]
def __repr__(self):
return (
f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId:"
f" {self.streamId}, role: {self.role}, title: {self.title}, invitedBy:"
f" {self.user.name if self.user else None})"
)
def __str__(self) -> str:
return self.__repr__()
class ProjectCollaborator(BaseModel):
id: str
role: str
user: LimitedUser
class Version(BaseModel):
authorUser: Optional[LimitedUser]
createdAt: datetime
id: str
message: Optional[str]
previewUrl: str
referencedObject: str
sourceApplication: Optional[str]
class Model(BaseModel):
author: LimitedUser
createdAt: datetime
description: Optional[str]
displayName: str
id: str
name: str
previewUrl: Optional[str]
updatedAt: datetime
class ModelWithVersions(Model):
versions: ResourceCollection[Version]
class Project(BaseModel):
allowPublicComments: bool
createdAt: datetime
description: Optional[str]
id: str
name: str
role: Optional[str]
sourceApps: List[str]
updatedAt: datetime
visibility: ProjectVisibility
workspaceId: Optional[str]
class ProjectWithModels(Project):
models: ResourceCollection[Model]
class ProjectWithTeam(Project):
invitedTeam: List[PendingStreamCollaborator]
team: List[ProjectCollaborator]
class ProjectCommentCollection(ResourceCollection[T], Generic[T]):
totalArchivedCount: int
class UserSearchResultCollection(BaseModel):
items: List[LimitedUser]
cursor: Optional[str] = None
@@ -1,9 +1,14 @@
from datetime import datetime
from typing import List, Optional
from deprecated import deprecated
from pydantic import BaseModel, Field
FE1_DEPRECATION_REASON = "Stream/Branch/Commit API is now deprecated, Use the new Project/Model/Version API functions in Client}"
FE1_DEPRECATION_VERSION = "2.20"
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Collaborator(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
@@ -11,6 +16,7 @@ class Collaborator(BaseModel):
avatar: Optional[str] = None
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Commit(BaseModel):
id: Optional[str] = None
message: Optional[str] = None
@@ -35,12 +41,14 @@ class Commit(BaseModel):
return self.__repr__()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Commits(BaseModel):
totalCount: Optional[int] = None
cursor: Optional[datetime] = None
items: List[Commit] = []
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Object(BaseModel):
id: Optional[str] = None
speckleType: Optional[str] = None
@@ -49,6 +57,7 @@ class Object(BaseModel):
createdAt: Optional[datetime] = None
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Branch(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
@@ -56,12 +65,14 @@ class Branch(BaseModel):
commits: Optional[Commits] = None
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Branches(BaseModel):
totalCount: Optional[int] = None
cursor: Optional[datetime] = None
items: List[Branch] = []
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Stream(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
@@ -88,67 +99,14 @@ class Stream(BaseModel):
return self.__repr__()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Streams(BaseModel):
totalCount: Optional[int] = None
cursor: Optional[datetime] = None
items: List[Stream] = []
class User(BaseModel):
id: Optional[str] = None
email: Optional[str] = None
name: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
streams: Optional[Streams] = None
def __repr__(self):
return (
f"User( id: {self.id}, name: {self.name}, email: {self.email}, company:"
f" {self.company} )"
)
def __str__(self) -> str:
return self.__repr__()
class LimitedUser(BaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
name: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
class PendingStreamCollaborator(BaseModel):
id: Optional[str] = None
inviteId: Optional[str] = None
streamId: Optional[str] = None
streamName: Optional[str] = None
title: Optional[str] = None
role: Optional[str] = None
invitedBy: Optional[User] = None
user: Optional[User] = None
token: Optional[str] = None
def __repr__(self):
return (
f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId:"
f" {self.streamId}, role: {self.role}, title: {self.title}, invitedBy:"
f" {self.user.name if self.user else None})"
)
def __str__(self) -> str:
return self.__repr__()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Activity(BaseModel):
actionType: Optional[str] = None
info: Optional[dict] = None
@@ -169,6 +127,7 @@ class Activity(BaseModel):
return self.__repr__()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class ActivityCollection(BaseModel):
totalCount: Optional[int] = None
items: Optional[List[Activity]] = None
@@ -183,16 +142,3 @@ class ActivityCollection(BaseModel):
def __str__(self) -> str:
return self.__repr__()
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
url: Optional[str] = None
description: Optional[str] = None
adminContact: Optional[str] = None
canonicalUrl: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
@@ -0,0 +1,36 @@
from typing import Optional
from pydantic import BaseModel
from specklepy.core.api.enums import (
ProjectModelsUpdatedMessageType,
ProjectUpdatedMessageType,
ProjectVersionsUpdatedMessageType,
UserProjectsUpdatedMessageType,
)
from specklepy.core.api.models.current import Model, Project, Version
class UserProjectsUpdatedMessage(BaseModel):
id: str
type: UserProjectsUpdatedMessageType
project: Optional[Project]
class ProjectModelsUpdatedMessage(BaseModel):
id: str
type: ProjectModelsUpdatedMessageType
model: Optional[Model]
class ProjectUpdatedMessage(BaseModel):
id: str
type: ProjectUpdatedMessageType
project: Optional[Project]
class ProjectVersionsUpdatedMessage(BaseModel):
id: str
type: ProjectVersionsUpdatedMessageType
modelId: Optional[str]
version: Optional[Version]
+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
+35 -1
View File
@@ -1,9 +1,10 @@
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
from gql.client import Client
from gql.transport.exceptions import TransportQueryError
from graphql import DocumentNode
from pydantic import BaseModel
from specklepy.core.api.credentials import Account
from specklepy.logging.exceptions import (
@@ -14,6 +15,8 @@ from specklepy.logging.exceptions import (
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
T = TypeVar("T", bound=BaseModel)
class ResourceBase(object):
def __init__(
@@ -43,6 +46,35 @@ class ResourceBase(object):
response = response[key]
return response
def make_request_and_parse_response(
self,
schema: Type[T],
query: DocumentNode,
variables: Optional[Dict[str, Any]] = None,
) -> T:
try:
with self.__lock:
response = self.client.execute(query, variable_values=variables)
except TransportQueryError as ex:
raise GraphQLException(
message=(
f"Failed to execute the GraphQL {self.name} request. Errors:"
f" {ex.errors}"
),
errors=ex.errors,
data=ex.data,
) from ex
except Exception as ex:
raise SpeckleException(
message=(
f"Failed to execute the GraphQL {self.name} request. Inner"
f" exception: {ex}"
),
exception=ex,
) from ex
return schema.model_validate(response)
def _parse_response(self, response: Union[dict, list, None], schema=None):
"""Try to create a class instance from the response"""
if response is None:
@@ -69,6 +101,8 @@ class ResourceBase(object):
parse_response: bool = True,
) -> Any:
"""Executes the GraphQL query"""
# This method has quite complex and ambiguous typing, and counter-intuitive error handling
# We are going to phase it out in favour of `make_request_and_parse_response`
try:
with self.__lock:
response = self.client.execute(query, variable_values=params)
@@ -0,0 +1,43 @@
from specklepy.core.api.resources.current.active_user_resource import ActiveUserResource
from specklepy.core.api.resources.current.model_resource import ModelResource
from specklepy.core.api.resources.current.other_user_resource import OtherUserResource
from specklepy.core.api.resources.current.project_invite_resource import (
ProjectInviteResource,
)
from specklepy.core.api.resources.current.project_resource import ProjectResource
from specklepy.core.api.resources.current.server_resource import ServerResource
from specklepy.core.api.resources.current.subscription_resource import (
SubscriptionResource,
)
from specklepy.core.api.resources.current.version_resource import VersionResource
from specklepy.core.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
__all__ = [
"ActiveUserResource",
"ModelResource",
"OtherUserResource",
"ProjectInviteResource",
"ProjectResource",
"ServerResource",
"SubscriptionResource",
"VersionResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
@@ -1,17 +1,30 @@
from datetime import datetime, timezone
from typing import List, Optional
from typing import List, Optional, overload
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Project,
ResourceCollection,
User,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.responses import DataResponse
from specklepy.logging.exceptions import GraphQLException
NAME = "active_user"
class Resource(ResourceBase):
"""API Access class for users"""
class ActiveUserResource(ResourceBase):
"""API Access class for the active user"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -23,38 +36,73 @@ class Resource(ResourceBase):
)
self.schema = User
def get(self) -> User:
"""Gets the profile of a user. If no id argument is provided,
will return the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
def get(self) -> Optional[User]:
"""Gets the currently active user profile (as extracted from the authorization header)
Returns:
User -- the retrieved user
User -- the requested user, or none if no authentication token is provided to the Client
"""
query = gql(
QUERY = gql(
"""
query User {
activeUser {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
"""
data:activeUser {
id
email
name
bio
company
avatar
verified
role
}
}
"""
)
params = {}
variables = {}
return self.make_request(query=query, params=params, return_type="activeUser")
return self.make_request_and_parse_response(
DataResponse[Optional[User]], QUERY, variables
).data
def _update(self, input: UserUpdateInput) -> User:
QUERY = gql(
"""
mutation ActiveUserMutations($input: UserUpdateInput!) {
data:activeUserMutations {
data:update(user: $input) {
id
email
name
bio
company
avatar
verified
role
}
}
}
"""
)
variables = {"input": input.model_dump(warnings="error")}
return self.make_request_and_parse_response(
DataResponse[DataResponse[User]], QUERY, variables
).data.data
@deprecated("Use UserUpdateInput overload", version=FE1_DEPRECATION_VERSION)
@overload
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
) -> User: ...
@overload
def update(self, *, input: UserUpdateInput) -> User: ...
def update(
self,
@@ -62,40 +110,125 @@ class Resource(ResourceBase):
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
"""
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
userUpdate(user: $user)
}
"""
)
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
params = {"user": {k: v for k, v in params.items() if v is not None}}
if not params["user"]:
return SpeckleException(
message=(
"You must provide at least one field to update your user profile"
*,
input: Optional[UserUpdateInput] = None,
) -> User:
if isinstance(input, UserUpdateInput):
return self._update(input=input)
else:
return self._update(
input=UserUpdateInput(
name=name,
company=company,
bio=bio,
avatar=avatar,
)
)
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
def get_projects(
self,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[UserProjectsFilter] = None,
) -> ResourceCollection[Project]:
QUERY = gql(
"""
query User($limit : Int!, $cursor: String, $filter: UserProjectsFilter) {
data:activeUser {
data:projects(limit: $limit, cursor: $cursor, filter: $filter) {
totalCount
cursor
items {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
}
}
}
"""
)
variables = {
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error") if filter else None,
}
response = self.make_request_and_parse_response(
DataResponse[Optional[DataResponse[ResourceCollection[Project]]]],
QUERY,
variables,
)
if response.data is None:
raise GraphQLException(
"GraphQL response indicated that the ActiveUser could not be found"
)
return response.data.data
def get_project_invites(self) -> List[PendingStreamCollaborator]:
QUERY = gql(
"""
query ProjectInvites {
data:activeUser {
data:projectInvites {
id
inviteId
invitedBy {
avatar
bio
company
id
name
role
verified
}
projectId
projectName
role
title
token
user {
id
name
bio
company
verified
avatar
role
}
}
}
}
"""
)
variables = {}
response = self.make_request_and_parse_response(
DataResponse[Optional[DataResponse[List[PendingStreamCollaborator]]]],
QUERY,
variables,
)
if response.data is None:
raise GraphQLException(
"GraphQL response indicated that the ActiveUser could not be found"
)
return response.data.data
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 20,
@@ -103,7 +236,7 @@ class Resource(ResourceBase):
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
@@ -174,6 +307,7 @@ class Resource(ResourceBase):
schema=ActivityCollection,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
@@ -194,13 +328,18 @@ class Resource(ResourceBase):
inviteId
streamId
streamName
projectId
projectName
title
role
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
@@ -213,6 +352,7 @@ class Resource(ResourceBase):
schema=PendingStreamCollaborator,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
@@ -237,15 +377,21 @@ class Resource(ResourceBase):
streamInvite(streamId: $streamId, token: $token) {
id
token
inviteId
streamId
streamName
projectId
projectName
title
role
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
@@ -0,0 +1,280 @@
from typing import Optional
from gql import gql
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
ModelVersionsFilter,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "model"
class ModelResource(ResourceBase):
"""API Access class for models"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
def get(self, model_id: str, project_id: str) -> Model:
QUERY = gql(
"""
query ModelGet($modelId: String!, $projectId: String!) {
data:project(id: $projectId) {
data:model(id: $modelId) {
id
name
previewUrl
updatedAt
description
displayName
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
}
}
"""
)
variables = {
"modelId": model_id,
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Model]], QUERY, variables
).data.data
def get_with_versions(
self,
model_id: str,
project_id: str,
*,
versions_limit: int = 25,
versions_cursor: Optional[str] = None,
versions_filter: Optional[ModelVersionsFilter] = None,
) -> ModelWithVersions:
QUERY = gql(
"""
query ModelGetWithVersions($modelId: String!, $projectId: String!, $versionsLimit: Int!, $versionsCursor: String, $versionsFilter: ModelVersionsFilter) {
data:project(id: $projectId) {
data:model(id: $modelId) {
id
name
previewUrl
updatedAt
versions(limit: $versionsLimit, cursor: $versionsCursor, filter: $versionsFilter) {
items {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
avatar
id
name
bio
company
verified
role
}
}
totalCount
cursor
}
description
displayName
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"modelId": model_id,
"versionsLimit": versions_limit,
"versionsCursor": versions_cursor,
"versionsFilter": (
versions_filter.model_dump(warnings="error")
if versions_filter
else None
),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[ModelWithVersions]], QUERY, variables
).data.data
def get_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ResourceCollection[Model]:
QUERY = gql(
"""
query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) {
data:project(id: $projectId) {
data:models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) {
items {
id
name
previewUrl
updatedAt
displayName
description
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
totalCount
cursor
}
}
}
"""
)
variables = {
"projectId": project_id,
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": (
models_filter.model_dump(warnings="error") if models_filter else None
),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[ResourceCollection[Model]]], QUERY, variables
).data.data
def create(self, input: CreateModelInput) -> Model:
QUERY = gql(
"""
mutation ModelCreate($input: CreateModelInput!) {
data:modelMutations {
data:create(input: $input) {
id
displayName
name
description
createdAt
updatedAt
previewUrl
author {
avatar
bio
company
id
name
role
verified
}
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Model]], QUERY, variables
).data.data
def delete(self, input: DeleteModelInput) -> bool:
QUERY = gql(
"""
mutation ModelDelete($input: DeleteModelInput!) {
data:modelMutations {
data:delete(input: $input)
}
}
"""
)
variables = {"input": input.model_dump(warnings="error")}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
).data.data
def update(self, input: UpdateModelInput) -> Model:
QUERY = gql(
"""
mutation ModelUpdate($input: UpdateModelInput!) {
data:modelMutations {
data:update(input: $input) {
id
name
displayName
description
createdAt
updatedAt
previewUrl
author {
avatar
bio
company
id
name
role
verified
}
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Model]], QUERY, variables
).data.data
@@ -1,16 +1,26 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import ActivityCollection, LimitedUser
from specklepy.core.api.models import (
ActivityCollection,
LimitedUser,
UserSearchResultCollection,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
from specklepy.logging.exceptions import SpeckleException
NAME = "other_user"
class Resource(ResourceBase):
class OtherUserResource(ResourceBase):
"""API Access class for other users, that are not the currently active user."""
def __init__(self, account, basepath, client, server_version) -> None:
@@ -23,7 +33,7 @@ class Resource(ResourceBase):
)
self.schema = LimitedUser
def get(self, id: str) -> LimitedUser:
def get(self, id: str) -> Optional[LimitedUser]:
"""
Gets the profile of another user.
@@ -33,26 +43,81 @@ class Resource(ResourceBase):
Returns:
LimitedUser -- the retrieved profile of another user
"""
query = gql(
QUERY = gql(
"""
query OtherUser($id: String!) {
otherUser(id: $id) {
id
name
bio
company
avatar
verified
role
}
query LimitedUser($id: String!) {
data:otherUser(id: $id){
id
name
bio
company
avatar
verified
role
}
}
"""
)
params = {"id": id}
variables = {"id": id}
return self.make_request(query=query, params=params, return_type="otherUser")
return self.make_request_and_parse_response(
DataResponse[Optional[LimitedUser]], QUERY, variables
).data
def user_search(
self,
query: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
archived: bool = False,
emailOnly: bool = False,
) -> UserSearchResultCollection:
"""Searches for a user on the server, by name or email. The search query must be at least
3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
cursor {Optional[str]} --
archived {bool} --
emailOnly {bool} --
Returns:
ResourceCollection[LimitedUser] -- User objects that match the search query
"""
QUERY = gql(
"""
query UserSearch($query: String!, $limit: Int!, $cursor: String, $archived: Boolean, $emailOnly: Boolean) {
data:userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived, emailOnly: $emailOnly) {
cursor
items {
id
name
bio
company
avatar
verified
role
}
}
}
"""
)
variables = {
"query": query,
"limit": limit,
"cursor": cursor,
"archived": archived,
"emailOnly": emailOnly,
}
return self.make_request_and_parse_response(
DataResponse[UserSearchResultCollection], QUERY, variables
).data
@deprecated(reason="Use user_search instead", version=FE1_DEPRECATION_VERSION)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
@@ -75,12 +140,13 @@ class Resource(ResourceBase):
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
id
name
bio
company
avatar
verified
role
}
}
}
@@ -92,6 +158,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["userSearch", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
user_id: str,
@@ -0,0 +1,254 @@
from typing import Any, Optional, Tuple
from gql import Client, gql
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import (
ProjectInviteCreateInput,
ProjectInviteUseInput,
)
from specklepy.core.api.models import PendingStreamCollaborator, ProjectWithTeam
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "project_invite"
class ProjectInviteResource(ResourceBase):
"""API Access class for project invites"""
def __init__(
self,
account: Account,
basepath: str,
client: Client,
server_version: Optional[Tuple[Any, ...]],
) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
def create(
self, project_id: str, input: ProjectInviteCreateInput
) -> ProjectWithTeam:
QUERY = gql(
"""
mutation ProjectInviteCreate($projectId: ID!, $input: ProjectInviteCreateInput!) {
data:projectMutations {
data:invites {
data:create(projectId: $projectId, input: $input) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
workspaceId
sourceApps
team {
id
role
user {
id
name
bio
company
avatar
verified
role
}
}
invitedTeam {
id
inviteId
projectId
projectName
title
role
token
user {
id
name
bio
company
avatar
verified
role
}
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[ProjectWithTeam]]], QUERY, variables
).data.data.data
def use(self, input: ProjectInviteUseInput) -> bool:
QUERY = gql(
"""
mutation ProjectInviteUse($input: ProjectInviteUseInput!) {
data:projectMutations {
data:invites {
data:use(input: $input)
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[bool]]], QUERY, variables
).data.data.data
def get(
self, project_id: str, token: Optional[str]
) -> Optional[PendingStreamCollaborator]:
"""Returns: The invite, or None if no invite exists"""
QUERY = gql(
"""
query ProjectInvite($projectId: String!, $token: String) {
data:projectInvite(projectId: $projectId, token: $token) {
id
inviteId
invitedBy {
avatar
bio
company
id
name
role
verified
}
projectId
projectName
role
title
token
user {
avatar
bio
company
id
name
role
verified
}
}
}
"""
)
variables = {
"projectId": project_id,
"token": token,
}
return self.make_request_and_parse_response(
DataResponse[Optional[PendingStreamCollaborator]], QUERY, variables
).data
def cancel(
self,
project_id: str,
invite_id: str,
) -> ProjectWithTeam:
QUERY = gql(
"""
mutation ProjectInviteCancel($projectId: ID!, $inviteId: String!) {
data:projectMutations {
data:invites {
data:cancel(projectId: $projectId, inviteId: $inviteId) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
team {
id
role
user {
id
name
bio
company
avatar
verified
role
}
}
invitedTeam {
id
inviteId
projectId
projectName
title
role
token
user {
id
name
bio
company
avatar
verified
role
}
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"inviteId": invite_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[ProjectWithTeam]]], QUERY, variables
).data.data.data
@@ -0,0 +1,336 @@
from typing import Optional
from gql import gql
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "project"
class ProjectResource(ResourceBase):
"""API Access class for projects"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
def get(self, project_id: str) -> Project:
QUERY = gql(
"""
query Project($projectId: String!) {
data:project(id: $projectId) {
allowPublicComments
createdAt
description
id
name
role
sourceApps
updatedAt
visibility
workspaceId
}
}
"""
)
variables = {
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[Project], QUERY, variables
).data
def get_with_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ProjectWithModels:
QUERY = gql(
"""
query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) {
data:project(id: $projectId) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) {
items {
id
name
previewUrl
updatedAt
displayName
description
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
cursor
totalCount
}
}
}
"""
)
variables = {
"projectId": project_id,
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": (
models_filter.model_dump(warnings="error") if models_filter else None
),
}
return self.make_request_and_parse_response(
DataResponse[ProjectWithModels], QUERY, variables
).data
def get_with_team(self, project_id: str) -> ProjectWithTeam:
QUERY = gql(
"""
query ProjectGetWithTeam($projectId: String!) {
data:project(id: $projectId) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
workspaceId
sourceApps
team {
id
role
user {
id
name
bio
company
avatar
verified
role
}
}
invitedTeam {
id
inviteId
projectId
projectName
title
role
token
user {
id
name
bio
company
avatar
verified
role
}
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
workspaceId
}
}
"""
)
variables = {
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[ProjectWithTeam], QUERY, variables
).data
def create(self, input: ProjectCreateInput) -> Project:
QUERY = gql(
"""
mutation ProjectCreate($input: ProjectCreateInput) {
data:projectMutations {
data:create(input: $input) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Project]], QUERY, variables
).data.data
def update(self, input: ProjectUpdateInput) -> Project:
QUERY = gql(
"""
mutation ProjectUpdate($input: ProjectUpdateInput!) {
data:projectMutations{
data:update(update: $input) {
allowPublicComments
createdAt
description
id
name
role
sourceApps
updatedAt
visibility
workspaceId
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Project]], QUERY, variables
).data.data
def delete(self, project_id: str) -> bool:
QUERY = gql(
"""
mutation ProjectDelete($projectId: String!) {
data:projectMutations {
data:delete(id: $projectId)
}
}
"""
)
variables = {
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
).data.data
def update_role(self, input: ProjectUpdateRoleInput) -> ProjectWithTeam:
QUERY = gql(
"""
mutation ProjectUpdateRole($input: ProjectUpdateRoleInput!) {
data:projectMutations {
data:updateRole(input: $input) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
team {
id
role
user {
id
name
bio
company
avatar
verified
role
}
}
invitedTeam {
id
inviteId
projectId
projectName
title
role
token
user {
id
name
bio
company
avatar
verified
role
}
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[ProjectWithTeam]], QUERY, variables
).data.data
@@ -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
@@ -10,7 +11,7 @@ from specklepy.logging.exceptions import GraphQLException
NAME = "server"
class Resource(ResourceBase):
class ServerResource(ResourceBase):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
@@ -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
@@ -0,0 +1,218 @@
from functools import wraps
from typing import Any, Callable, Dict, Optional, Sequence, Type
from gql import gql
from graphql import DocumentNode
from pydantic import BaseModel
from typing_extensions import TypeVar
from specklepy.core.api.models import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
from specklepy.logging.exceptions import SpeckleException
NAME = "subscribe"
TEventArgs = TypeVar("TEventArgs", bound=BaseModel)
def check_wsclient(function):
@wraps(function)
async def check_wsclient_wrapper(self, *args, **kwargs):
if self.client is None:
raise SpeckleException(
"You must authenticate before you can subscribe to events"
)
else:
return await function(self, *args, **kwargs)
return check_wsclient_wrapper
class SubscriptionResource(ResourceBase):
"""API Access class for subscriptions"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
)
async def user_projects_updated(
self, callback: Callable[[UserProjectsUpdatedMessage], None]
) -> None:
QUERY = gql(
"""
subscription UserProjectsUpdated {
data:userProjectsUpdated {
id
project {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
type
}
}
"""
)
await self.subscribe_2(
DataResponse[UserProjectsUpdatedMessage],
QUERY,
None,
callback=lambda d: callback(d.data),
)
async def project_models_updated(
self,
callback: Callable[[ProjectModelsUpdatedMessage], None],
id: str,
model_ids: Optional[Sequence[str]] = None,
) -> None:
QUERY = gql(
"""
subscription ProjectModelsUpdated($id: String!, $modelIds: [String!]) {
data:projectModelsUpdated(id: $id, modelIds: $modelIds) {
id
model {
id
name
previewUrl
updatedAt
description
displayName
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
type
}
}
"""
)
variables = {"id": id, "modelIds": model_ids}
await self.subscribe_2(
DataResponse[ProjectModelsUpdatedMessage],
QUERY,
variables,
callback=lambda d: callback(d.data),
)
async def project_updated(
self,
callback: Callable[[ProjectUpdatedMessage], None],
id: str,
) -> None:
QUERY = gql(
"""
subscription ProjectUpdated($id: String!) {
data:projectUpdated(id: $id) {
id
project {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
type
}
}
"""
)
variables = {"id": id}
await self.subscribe_2(
DataResponse[ProjectUpdatedMessage],
QUERY,
variables,
callback=lambda d: callback(d.data),
)
async def project_versions_updated(
self,
callback: Callable[[ProjectVersionsUpdatedMessage], None],
id: str,
) -> None:
QUERY = gql(
"""
subscription ProjectVersionsUpdated($id: String!) {
data:projectVersionsUpdated(id: $id) {
id
modelId
type
version {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
}
}
"""
)
variables = {"id": id}
await self.subscribe_2(
DataResponse[ProjectVersionsUpdatedMessage],
QUERY,
variables,
callback=lambda d: callback(d.data),
)
@check_wsclient
async def subscribe_2(
self,
response_type: Type[TEventArgs],
query: DocumentNode,
variables: Optional[Dict[str, Any]],
callback: Callable[[TEventArgs], None],
) -> None:
async with self.client as session:
self.session = session
gen = session.subscribe(query, variable_values=variables)
async for res in gen:
event_arg = response_type.model_validate(res)
callback(event_arg)
@@ -0,0 +1,234 @@
from typing import Optional
from gql import gql
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.inputs.version_inputs import (
CreateVersionInput,
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
from specklepy.core.api.models import ResourceCollection, Version
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "model"
class VersionResource(ResourceBase):
"""API Access class for model versions"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
def get(self, version_id: str, project_id: str) -> Version:
QUERY = gql(
"""
query VersionGet($projectId: String!, $versionId: String!) {
data:project(id: $projectId) {
data:version(id: $versionId) {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"versionId": version_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Version]], QUERY, variables
).data.data
def get_versions(
self,
model_id: str,
project_id: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[ModelVersionsFilter] = None,
) -> ResourceCollection[Version]:
QUERY = gql(
"""
query VersionGetVersions($projectId: String!, $modelId: String!, $limit: Int!, $cursor: String, $filter: ModelVersionsFilter) {
data:project(id: $projectId) {
data:model(id: $modelId) {
data:versions(limit: $limit, cursor: $cursor, filter: $filter) {
items {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
cursor
totalCount
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"modelId": model_id,
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error") if filter else None,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[ResourceCollection[Version]]]],
QUERY,
variables,
).data.data.data
def create(self, input: CreateVersionInput) -> str:
QUERY = gql(
"""
mutation Create($input: CreateVersionInput!) {
data:versionMutations {
data:create(input: $input) {
data:id
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[str]]], QUERY, variables
).data.data.data
def update(self, input: UpdateVersionInput) -> Version:
QUERY = gql(
"""
mutation VersionUpdate($input: UpdateVersionInput!) {
data:versionMutations {
data:update(input: $input) {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
}
}
"""
)
variables = {"input": input.model_dump(warnings="error")}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Version]], QUERY, variables
).data.data
def move_to_model(self, input: MoveVersionsInput) -> str:
QUERY = gql(
"""
mutation VersionMoveToModel($input: MoveVersionsInput!) {
data:versionMutations {
data:moveToModel(input: $input) {
data:id
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[str]]], QUERY, variables
).data.data.data
def delete(self, input: DeleteVersionsInput) -> bool:
QUERY = gql(
"""
mutation VersionDelete($input: DeleteVersionsInput!) {
data:versionMutations {
data:delete(input: $input)
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
).data.data
def received(self, input: MarkReceivedVersionInput) -> bool:
QUERY = gql(
"""
mutation MarkReceived($input: MarkReceivedVersionInput!) {
data:versionMutations {
data:markReceived(input: $input)
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
).data.data
@@ -0,0 +1,15 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import ActiveUserResource
@deprecated(
reason="Class renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION
)
class Resource(ActiveUserResource):
"""
Class renamed to ActiveUserResource
"""
pass
@@ -1,15 +1,24 @@
from typing import Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import Branch
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
Branch,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "branch"
class Resource(ResourceBase):
"""API Access class for branches"""
"""
API Access class for branches
Branch resource is deprecated, please use model resource instead
"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
@@ -20,6 +29,7 @@ class Resource(ResourceBase):
)
self.schema = Branch
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self, stream_id: str, name: str, description: str = "No description provided"
) -> str:
@@ -39,6 +49,8 @@ class Resource(ResourceBase):
}
"""
)
if len(name) < 3:
return SpeckleException(message="Branch Name must be at least 3 characters")
params = {
"branch": {
"streamId": stream_id,
@@ -51,6 +63,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type="branchCreate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, name: str, commits_limit: int = 10):
"""Get a branch by name from a stream
@@ -98,6 +111,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "branch"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
@@ -153,6 +167,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "branches", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
stream_id: str,
@@ -194,6 +209,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type="branchUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
@@ -1,15 +1,24 @@
from typing import List, Optional
from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import Commit
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
Commit,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "commit"
class Resource(ResourceBase):
"""API Access class for commits"""
"""
API Access class for commits
Commit resource is deprecated, please use version resource instead
"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
@@ -20,6 +29,7 @@ class Resource(ResourceBase):
)
self.schema = Commit
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, commit_id: str) -> Commit:
"""
Gets a commit given a stream and the commit id
@@ -58,6 +68,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "commit"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, limit: int = 10) -> List[Commit]:
"""
Get a list of commits on a given stream
@@ -99,6 +110,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "commits", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
stream_id: str,
@@ -106,8 +118,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
@@ -148,6 +160,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitCreate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
Update a commit
@@ -175,6 +188,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
Delete a commit
@@ -199,6 +213,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitDelete", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def received(
self,
stream_id: str,
@@ -0,0 +1,15 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import OtherUserResource
@deprecated(
reason="Class renamed to OtherUserResource", version=FE1_DEPRECATION_VERSION
)
class Resource(OtherUserResource):
"""
Class renamed to OtherUserResource
"""
pass
@@ -0,0 +1,11 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import ServerResource
NAME = "server"
@deprecated(reason="Renamed to ServerResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""API Access class for the server"""
@@ -4,7 +4,15 @@ 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.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
@@ -12,7 +20,10 @@ NAME = "stream"
class Resource(ResourceBase):
"""API Access class for streams"""
"""
API Access class for streams
Stream resource is deprecated, please use project resource instead
"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -25,6 +36,7 @@ class Resource(ResourceBase):
self.schema = Stream
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream:
"""Get the specified stream from the server
@@ -86,6 +98,7 @@ class Resource(ResourceBase):
return self.make_request(query=query, params=params, return_type="stream")
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_limit: int = 10) -> List[Stream]:
"""Get a list of the user's streams
@@ -98,7 +111,7 @@ class Resource(ResourceBase):
query = gql(
"""
query User($stream_limit: Int!) {
user {
activeUser {
id
bio
name
@@ -136,9 +149,10 @@ class Resource(ResourceBase):
params = {"stream_limit": stream_limit}
return self.make_request(
query=query, params=params, return_type=["user", "streams", "items"]
query=query, params=params, return_type=["activeUser", "streams", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
name: str = "Anonymous Python Stream",
@@ -163,7 +177,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}
}
@@ -172,6 +187,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamCreate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
id: str,
@@ -212,6 +228,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, id: str) -> bool:
"""Delete a stream given its id
@@ -235,6 +252,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamDelete", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def search(
self,
search_query: str,
@@ -314,6 +332,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["streams", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream.
@@ -347,6 +366,7 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["streamFavorite"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(
self, stream_id: str
) -> List[PendingStreamCollaborator]:
@@ -374,19 +394,27 @@ class Resource(ResourceBase):
inviteId
streamId
streamName
projectName
projectId
title
role
invitedBy{
id
name
bio
company
avatar
verified
role
}
user {
id
name
bio
company
avatar
verified
role
}
}
}
@@ -402,6 +430,7 @@ class Resource(ResourceBase):
schema=PendingStreamCollaborator,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite(
self,
stream_id: str,
@@ -458,6 +487,7 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_batch(
self,
stream_id: str,
@@ -504,11 +534,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(
@@ -518,6 +547,7 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite
@@ -549,6 +579,7 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite
@@ -586,6 +617,7 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream
@@ -632,6 +664,7 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
@@ -661,6 +694,7 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
stream_id: str,
@@ -728,13 +762,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(
@@ -1,11 +1,16 @@
from functools import wraps
from typing import Callable, Dict, List, Optional, Union
from deprecated import deprecated
from gql import gql
from graphql import DocumentNode
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
Stream,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.resources.stream import Stream
from specklepy.logging.exceptions import SpeckleException
NAME = "subscribe"
@@ -35,6 +40,7 @@ class Resource(ResourceBase):
name=NAME,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_added(self, callback: Optional[Callable] = None):
"""Subscribes to new stream added event for your profile.
@@ -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"
@@ -0,0 +1,2 @@
schema: https://app.speckle.systems/graphql
documents: '**/*.graphql'
+9
View File
@@ -0,0 +1,9 @@
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class DataResponse(BaseModel, Generic[T]):
data: T
+124 -28
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
@@ -28,7 +30,7 @@ class StreamWrapper:
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
@@ -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}'"
)
@@ -1,6 +1,7 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
+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):
-22
View File
@@ -1,22 +0,0 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.layers import (
VectorLayer,
RasterLayer,
)
from specklepy.objects.GIS.geometry import (
GisPolygonGeometry,
GisPolygonElement,
GisLineElement,
GisPointElement,
GisRasterElement,
)
from specklepy.objects.GIS.CRS import (
CRS,
)
__all__ = ["VectorLayer", "RasterLayer",
"GisPolygonGeometry", "GisPolygonElement", "GisLineElement", "GisPointElement", "GisRasterElement",
"CRS"]
-6
View File
@@ -1,6 +0,0 @@
"""Builtin Speckle object kit."""
from specklepy.objects import encoding, geometry, other, primitive, structural, units
from specklepy.objects.base import Base
__all__ = ["Base", "encoding", "geometry", "other", "units", "structural", "primitive"]
+28 -33
View File
@@ -1,4 +1,5 @@
import contextlib
from dataclasses import dataclass, field
from enum import Enum
from inspect import isclass
from typing import (
@@ -18,8 +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.logging.exceptions import SpeckleException
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
@@ -65,7 +65,6 @@ REMOVE_FROM_DIR = {
"_handle_object_count",
"_type_check",
"_type_registry",
"_units",
"add_chunkable_attrs",
"add_detachable_attrs",
"get_children_count",
@@ -116,7 +115,7 @@ class _RegisteringBase:
@classmethod
def _determine_speckle_type(cls) -> str:
"""
This method brings the speckle_type construction in par with peckle-sharp/Core.
This method brings the speckle_type construction in par with Speckle-sharp/Core.
The implementation differs, because in Core the basis of the speckle_type if
type.FullName, which includes the dotnet namespace name too.
@@ -168,8 +167,11 @@ class _RegisteringBase:
initialization. This is reused to register each subclassing type into a class
level dictionary.
"""
# if not speckle_type:
# raise Exception("no type")
cls._speckle_type_override = speckle_type
cls.speckle_type = cls._determine_speckle_type()
# cls.speckle_type = speckle_type
if cls._full_name() in cls._type_registry:
raise ValueError(
f"The speckle_type: {speckle_type} is already registered for type: "
@@ -188,7 +190,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")
@@ -318,22 +321,17 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
return False, value
class Base(_RegisteringBase):
@dataclass(kw_only=True)
class Base(_RegisteringBase, speckle_type="Base"):
id: Union[str, None] = None
totalChildrenCount: Union[int, None] = None
# totalChildrenCount: Union[int, None] = None
applicationId: Union[str, None] = None
_units: Union[None, str] = None
def __init__(self, **kwargs) -> None:
super().__init__()
for k, v in kwargs.items():
self.__setattr__(k, v)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(id: {self.id}, "
f"speckle_type: {self.speckle_type}, "
f"totalChildrenCount: {self.totalChildrenCount})"
# f"totalChildrenCount: {self.totalChildrenCount})"
)
def __str__(self) -> str:
@@ -461,21 +459,21 @@ class Base(_RegisteringBase):
"""
self._detachable = self._detachable.union(names)
@property
def units(self) -> Union[str, None]:
return self._units
# @property
# def units(self) -> Union[str, None]:
# return self._units
@units.setter
def units(self, value: Union[str, Units, None]):
"""While this property accepts any string value, geometry expects units to be specific strings (see Units enum)"""
if isinstance(value, str) or value is None:
self._units = value
elif isinstance(value, Units):
self._units = value.value
else:
raise SpeckleInvalidUnitException(
f"Unknown type {type(value)} received for units"
)
# @units.setter
# def units(self, value: Union[str, Units, None]):
# """While this property accepts any string value, geometry expects units to be specific strings (see Units enum)"""
# if isinstance(value, str) or value is None:
# self._units = value
# elif isinstance(value, Units):
# self._units = value.value
# else:
# raise SpeckleInvalidUnitException(
# f"Unknown type {type(value)} received for units"
# )
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
@@ -567,9 +565,6 @@ class Base(_RegisteringBase):
Base.update_forward_refs()
@dataclass(kw_only=True)
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: Union[List[Any], None] = None
def __init__(self) -> None:
super().__init__()
self.data = []
data: List[Any] = field(default_factory=list)
File diff suppressed because it is too large Load Diff
+110
View File
@@ -0,0 +1,110 @@
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass, field
from typing import Generic, List, TypeVar
from specklepy.logging.exceptions import SpeckleInvalidUnitException
from specklepy.objects.base import Base
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
T = TypeVar("T") # define type variable for generic type
# generic interfaces
class ICurve(metaclass=ABCMeta):
@property
@abstractmethod
def length(self) -> float:
pass
@property
@abstractmethod
def _domain(self) -> Interval:
pass
@property
@abstractmethod
def units(self) -> str:
pass
class IDisplayValue(Generic[T], metaclass=ABCMeta):
@property
@abstractmethod
def display_value(self) -> T:
pass
@dataclass(kw_only=True)
class IHasUnits(metaclass=ABCMeta):
units: str | Units
_units: str = field(repr=False, init=False)
@property
def units(self) -> str:
return self._units
@units.setter
def units(self, value: str | Units):
if isinstance(value, str):
self._units = value
elif isinstance(value, Units):
self._units = value.value
else:
raise SpeckleInvalidUnitException(
f"Unknown type {type(value)} received for units"
)
@dataclass(kw_only=True)
class IHasArea(metaclass=ABCMeta):
area: float
_area: float = field(init=False, repr=False)
@property
def area(self) -> float:
return self._area
@area.setter
def area(self, value: float):
if not isinstance(value, (int, float)):
raise ValueError(f"Area must be a number, got {type(value)}")
self._area = float(value)
@dataclass(kw_only=True)
class IHasVolume(metaclass=ABCMeta):
volume: float
_volume: float = field(init=False, repr=False)
@property
def volume(self) -> float:
return self._volume
@volume.setter
def volume(self, value: float):
if not isinstance(value, (int, float)):
raise ValueError(f"Volume must be a number, got {type(value)}")
self._volume = float(value)
# data object interfaces
class IProperties(metaclass=ABCMeta):
@property
@abstractmethod
def properties(self) -> dict[str, object]:
pass
class IDataObject(IProperties, IDisplayValue[List[Base]], metaclass=ABCMeta):
@property
@abstractmethod
def name(self) -> str:
pass
class IBlenderObject(IDataObject, metaclass=ABCMeta):
@property
@abstractmethod
def type(self) -> str:
pass
@@ -0,0 +1,35 @@
from dataclasses import dataclass, field
from typing import List
from specklepy.objects.base import Base
@dataclass(kw_only=True)
class Collection(
Base,
# TODO: add deprecated speckle_types
speckle_type="Speckle.Core.Models.Collections.Collection",
detachable={"elements"},
):
"""
A simple container for organising objects within a model and preserving object hierarchy.
A container is defined by a human-readable name a unique applicationId and its list of contained elements.
The elements can include an unrestricted number of Base objects including additional nested Collections.
Note:
A Collection can be for example a Layer in Rhino/AutoCad, a collection in Blender, or a Category in Revit.
The location of each collection in the hierarchy of collections in a commit will be retrieved through commit traversal.
Attributes:
name: The human-readable name of the Collection. This name is not necessarily unique within a commit. Set the applicationId for a unique identifier.
elements: The elements contained in this Collection. This may include additional nested Collections
"""
name: str
elements: List[Base] = field(default_factory=list)
if __name__ == "__main__":
c = Collection(name="asfd")
print(c)
@@ -0,0 +1,18 @@
from abc import ABCMeta, abstractmethod
class ISpeckleObject(speckle_type="ISpeckleObjects", metaclass=ABCMeta):
@property
@abstractmethod
def id(self) -> str | None:
pass
@property
@abstractmethod
def application_id(self) -> str | None:
pass
@property
@abstractmethod
def speckle_type(self) -> str:
pass
@@ -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]
+17 -14
View File
@@ -1,25 +1,28 @@
from typing import Any, List
from dataclasses import dataclass
from typing import List
from specklepy.objects.base import Base
NAMESPACE = "Objects.Primitive"
@dataclass(kw_only=True)
class Interval(Base, speckle_type="Objects.Primitive.Interval"):
start: float = 0.0 # Added default
end: float = 0.0 # Added default
class Interval(Base, speckle_type=f"{NAMESPACE}.Interval"):
start: float = 0.0
end: float = 0.0
@property
def length(self) -> float:
return abs(self.end - self.start)
def length(self):
return abs(self.start - self.end)
def __str__(self) -> str:
return f"{super().__str__()}[{self.start}, {self.end}]"
@classmethod
def from_list(cls, args: List[Any]) -> "Interval":
return cls(start=args[0], end=args[1])
def unit_interval(cls) -> "Interval":
return cls(start=0, end=1)
def to_list(self) -> List[Any]:
def to_list(self) -> List[float]:
return [self.start, self.end]
class Interval2d(Base, speckle_type=f"{NAMESPACE}.Interval2d"):
u: Interval
v: Interval
@classmethod
def from_list(cls, args: List[float]) -> "Interval":
return cls(start=args[0], end=args[1])
+48
View File
@@ -0,0 +1,48 @@
from dataclasses import dataclass, field
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class ColorProxy(
Base,
speckle_type="Models.Proxies.ColorProxy",
detachable={"objects"},
):
objects: List[str] = field(default_factory=list)
value: int
name: Optional[str] = None
@dataclass(kw_only=True)
class GroupProxy(
Base,
speckle_type="Models.Proxies.GroupProxy",
detachable={"objects"},
):
objects: List[str] = field(default_factory=list)
name: str = field(default="Unnamed Group")
@dataclass(kw_only=True)
class InstanceProxy(
Base,
IHasUnits,
speckle_type="Models.Proxies.InstanceProxy",
):
definition_id: str
transform: List[float] = field(default_factory=list)
max_depth: int = 50
@dataclass(kw_only=True)
class InstanceDefinitionProxy(
Base,
speckle_type="Models.Proxies.InstanceDefinitionProxy",
detachable={"objects"},
):
objects: List[str] = field(default_factory=list)
max_depth: int = 50
name: str = field(default="Unnamed Instance")
+26
View File
@@ -0,0 +1,26 @@
from devtools import debug
from specklepy.api.operations import deserialize, serialize
from specklepy.objects.geometry import Line, Point
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
# points
p1 = Point(x=1.0, y=2.0, z=3.0, units=Units.m)
p2 = Point(x=4.0, y=6.0, z=8.0, units=Units.m, applicationId="asdf")
# test Line
line = Line(start=p1, end=p2, units=Units.m, domain=Interval(start=0.0, end=1.0))
print(f"\nLine length: {line.length}")
ser_line = serialize(line)
line_again = deserialize(ser_line)
print("\nOriginal line:")
debug(line)
print("\nSerialized line:")
debug(ser_line)
print("\nDeserialized line:")
debug(line_again)
+182
View File
@@ -0,0 +1,182 @@
from devtools import debug
from specklepy.api.operations import deserialize, serialize
from specklepy.objects.geometry import Mesh
# create a speckle cube mesh (but more colorful)
vertices = [
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
1.0,
1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
1.0,
1.0,
0.0,
1.0,
1.0,
1.0,
1.0,
0.0,
1.0,
1.0,
]
# define faces (triangles)
faces = [
3,
0,
1,
2,
3,
0,
2,
3,
3,
4,
5,
6,
3,
4,
6,
7,
3,
0,
4,
7,
3,
0,
7,
3,
3,
1,
5,
6,
3,
1,
6,
2,
3,
3,
2,
6,
3,
3,
6,
7,
3,
0,
1,
5,
3,
0,
5,
4,
]
# create colors (one per vertex)
colors = [
255,
0,
0,
255,
0,
255,
0,
255,
0,
0,
255,
255,
255,
255,
0,
255,
255,
0,
255,
255,
0,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
255,
]
texture_coordinates = [
0.0,
0.0,
1.0,
0.0,
1.0,
1.0,
0.0,
1.0,
0.0,
0.0,
1.0,
0.0,
1.0,
1.0,
0.0,
1.0,
]
# create the mesh
cube_mesh = Mesh(
vertices=vertices,
faces=faces,
colors=colors,
textureCoordinates=texture_coordinates,
units="mm",
area=0.0,
volume=0.0,
)
print("\nMesh Details:")
print(f"Number of vertices: {cube_mesh.vertices_count}")
print(f"Number of texture coordinates: {cube_mesh.texture_coordinates_count}")
print("\nSome vertex points:")
for i in range(4):
point = cube_mesh.get_point(i)
print(f"Vertex {i}: ({point.x}, {point.y}, {point.z})")
print("\nSome texture coordinates:")
for i in range(4):
u, v = cube_mesh.get_texture_coordinate(i)
print(f"Texture coordinate {i}: ({u}, {v})")
print("\nTesting serialization...")
ser_mesh = serialize(cube_mesh)
mesh_again = deserialize(ser_mesh)
print("\nOriginal mesh:")
debug(cube_mesh)
print("\nDeserialized mesh:")
debug(mesh_again)
print("\nTesting vertex-texture coordinate alignment...")
cube_mesh.align_vertices_with_texcoords_by_index()
print("Alignment complete.")
print(f"Vertices count after alignment: {cube_mesh.vertices_count}")
print(
f"Texture coordinates count after alignment: {cube_mesh.texture_coordinates_count}"
)
+21
View File
@@ -0,0 +1,21 @@
from devtools import debug
from specklepy.api.operations import deserialize, serialize
from specklepy.objects.geometry import Point
from specklepy.objects.models.units import Units
# test points
p1 = Point(x=1.0, y=2.0, z=3.0, units=Units.m)
p2 = Point(x=4.0, y=6.0, z=8.0, units=Units.m, applicationId="asdf")
print("Distance between points:", p1.distance_to(p2))
ser_p1 = serialize(p1)
p1_again = deserialize(ser_p1)
print("\nOriginal point:")
debug(p1)
print("\nSerialized point:")
debug(ser_p1)
print("\nDeserialized point:")
debug(p1_again)
@@ -0,0 +1,51 @@
from devtools import debug
from specklepy.api.operations import deserialize, serialize
from specklepy.objects.geometry import Polyline
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
# create points for first polyline - not closed, in meters
points1_coords = [1.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 2.0, 0.0, 1.0, 2.0, 0.0]
# Create points for second polyline - closed, in ft
points2_coords = [0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 3.0, 3.0, 0.0, 0.0, 3.0, 0.0]
# create polylines
polyline1 = Polyline(
value=points1_coords,
closed=False,
units=Units.m,
domain=Interval(start=0.0, end=1.0),
)
polyline2 = Polyline(
value=points2_coords,
closed=True,
units=Units.feet,
domain=Interval(start=0.0, end=1.0),
applicationId="polyllllineeee",
)
print("Polyline 1 length (meters):", polyline1.length)
print("Polyline 2 length (feet):", polyline2.length)
ser_poly1 = serialize(polyline1)
poly1_again = deserialize(ser_poly1)
print("\nOriginal polyline 1:")
debug(polyline1)
print("\nSerialized polyline 1:")
debug(ser_poly1)
print("\nDeserialized polyline 1:")
debug(poly1_again)
ser_poly2 = serialize(polyline2)
poly2_again = deserialize(ser_poly2)
print("\nOriginal polyline 2:")
debug(polyline2)
print("\nSerialized polyline 2:")
debug(ser_poly2)
print("\nDeserialized polyline 2:")
debug(poly2_again)
@@ -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
+24
View File
@@ -0,0 +1,24 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.GIS.geometry import (
GisLineElement,
GisPointElement,
GisPolygonElement,
GisPolygonGeometry,
GisRasterElement,
PolygonGeometry,
)
from specklepy.objects.GIS.layers import RasterLayer, VectorLayer
__all__ = [
"VectorLayer",
"RasterLayer",
"GisPolygonGeometry",
"PolygonGeometry",
"GisPolygonElement",
"GisLineElement",
"GisPointElement",
"GisRasterElement",
"CRS",
]
@@ -1,15 +1,26 @@
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 PolygonGeometry(Base, speckle_type="Objects.GIS.PolygonGeometry"):
"""GIS Polygon Geometry"""
boundary: Optional[Union[Polyline, Arc, Line, Circle, Polycurve]] = None
voids: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]] ] = None
displayValue: Optional[List[Mesh]] = None
boundary: Optional[Polyline]
voids: Optional[List[Polyline]]
GisPolygonGeometry = PolygonGeometry
class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"):
"""GIS Polygon element"""
@@ -17,19 +28,24 @@ class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"):
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
@@ -1,23 +1,26 @@
from typing import Any, Dict, List, Union, 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="VectorLayer",
serialize_ignore={"features"}):
Collection,
detachable={"elements"},
speckle_type="VectorLayer",
serialize_ignore={"features"},
):
"""GIS Vector Layer"""
name: Optional[str]=None
crs: Optional[Union[CRS, Base]]=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,25 +59,25 @@ class VectorLayer(
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class RasterLayer(
Collection,
detachable={"elements"},
speckle_type="RasterLayer",
serialize_ignore={"features"}):
Collection,
detachable={"elements"},
speckle_type="RasterLayer",
serialize_ignore={"features"},
):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]]=None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]]=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]]:
@@ -83,23 +88,23 @@ class RasterLayer(
self.elements = value
class VectorLayer(
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.VectorLayer",
serialize_ignore={"features"}):
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
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]]:
@@ -109,24 +114,24 @@ 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"}):
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
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]]=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]]:
@@ -135,4 +140,3 @@ class RasterLayer(
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
+23
View File
@@ -0,0 +1,23 @@
"""Builtin Speckle object kit."""
# 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",
# "GIS",
]
+588
View File
@@ -0,0 +1,588 @@
import contextlib
from enum import Enum
from inspect import isclass
from typing import (
Any,
ClassVar,
Dict,
ForwardRef,
List,
Optional,
Set,
Tuple,
Type,
Union,
get_type_hints,
)
from warnings import warn
from stringcase import pascalcase
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.units import Units
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
# to remove from dir() when calling get_member_names()
REMOVE_FROM_DIR = {
"Config",
"_Base__dict_helper",
"__annotations__",
"__class__",
"__delattr__",
"__dict__",
"__dir__",
"__doc__",
"__eq__",
"__format__",
"__ge__",
"__getattribute__",
"__getitem__",
"__gt__",
"__hash__",
"__init__",
"__init_subclass__",
"__le__",
"__lt__",
"__module__",
"__ne__",
"__new__",
"__reduce__",
"__reduce_ex__",
"__repr__",
"__setattr__",
"__setitem__",
"__sizeof__",
"__str__",
"__subclasshook__",
"__weakref__",
"_chunk_size_default",
"_chunkable",
"_count_descendants",
"_attr_types",
"_detachable",
"_handle_object_count",
"_type_check",
"_type_registry",
"_units",
"add_chunkable_attrs",
"add_detachable_attrs",
"get_children_count",
"get_dynamic_member_names",
"get_id",
"get_member_names",
"get_registered_type",
"get_typed_member_names",
"to_dict",
"update_forward_refs",
"validate_prop_name",
"from_list",
"to_list",
}
class _RegisteringBase:
"""
Private Base model for Speckle types.
This is an implementation detail, please do not use this outside this module.
This class provides automatic registration of `speckle_type` into a global,
(class level) registry for each subclassing type.
The type registry is a base for accurate type based (de)serialization.
"""
speckle_type: ClassVar[str]
_speckle_type_override: ClassVar[Optional[str]] = None
_speckle_namespace: ClassVar[Optional[str]] = None
_type_registry: ClassVar[Dict[str, Type["Base"]]] = {}
_attr_types: ClassVar[Dict[str, Type]] = {}
# dict of chunkable props and their max chunk size
_chunkable: Dict[str, int] = {}
_chunk_size_default: int = 1000
_detachable: Set[str] = set() # list of defined detachable props
_serialize_ignore: Set[str] = set()
@classmethod
def get_registered_type(cls, speckle_type: str) -> Optional[Type["Base"]]:
"""Get the registered type from the protected mapping via the `speckle_type`"""
for full_name in reversed(speckle_type.split(":")):
maybe_type = cls._type_registry.get(full_name, None)
if maybe_type:
return maybe_type
return None
@classmethod
def _determine_speckle_type(cls) -> str:
"""
This method brings the speckle_type construction in par with Speckle-sharp/Core.
The implementation differs, because in Core the basis of the speckle_type if
type.FullName, which includes the dotnet namespace name too.
Copying that behavior is hard in python, where the concept of namespaces
means something entirely different.
So we enabled a speckle_type override mechanism, that enables
"""
base_name = "Base"
if cls.__name__ == base_name:
return base_name
bases = [
b._full_name()
for b in reversed(cls.mro())
if issubclass(b, Base) and b.__name__ != base_name
]
return ":".join(bases)
@classmethod
def _full_name(cls) -> str:
base_name = "Base"
if cls.__name__ == base_name:
return base_name
if cls._speckle_type_override:
return cls._speckle_type_override
# convert the module names to PascalCase to match c# namespace naming convention
# also drop specklepy from the beginning
namespace = ".".join(
pascalcase(m)
for m in filter(lambda name: name != "specklepy", cls.__module__.split("."))
)
return f"{namespace}.{cls.__name__}"
def __init_subclass__(
cls,
speckle_type: Optional[str] = None,
chunkable: Optional[Dict[str, int]] = None,
detachable: Optional[Set[str]] = None,
serialize_ignore: Optional[Set[str]] = None,
**kwargs: Dict[str, Any],
):
"""
Hook into subclass type creation.
This is provides a mechanism to hook into the event of the subclass type object
initialization. This is reused to register each subclassing type into a class
level dictionary.
"""
# if not speckle_type:
# raise Exception("no type")
cls._speckle_type_override = speckle_type
cls.speckle_type = cls._determine_speckle_type()
# cls.speckle_type = speckle_type
if cls._full_name() in cls._type_registry:
raise ValueError(
f"The speckle_type: {speckle_type} is already registered for type: "
f"{cls._type_registry[cls._full_name()].__name__}. "
"Please choose a different type name."
)
cls._type_registry[cls._full_name()] = cls # type: ignore
try:
cls._attr_types = get_type_hints(cls)
except Exception:
cls._attr_types = getattr(cls, "__annotations__", {})
if chunkable:
chunkable = {k: v for k, v in chunkable.items() if isinstance(v, int)}
cls._chunkable = dict(cls._chunkable, **chunkable)
if detachable:
cls._detachable = cls._detachable.union(detachable)
if serialize_ignore:
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
# we know, that the super here is object, that takes no args on init subclass
return super().__init_subclass__()
# T = TypeVar("T")
# how i wish the code below would be correct, but we're also parsing into floats
# and converting into strings if the original type is string, but the value isn't
# def _validate_type(t: type, value: T) -> Tuple[bool, T]:
def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
# this should be reworked. Its only ok to return null for Optionals...
# if t is None and value is None:
if value is None:
return True, value
# after fixing the None t above, this should be
# if t is Any:
# if t is None:
if t is None or t is Any:
return True, value
if isclass(t) and issubclass(t, Enum):
if isinstance(value, t):
return True, value
if value in t._value2member_map_:
return True, t(value)
if getattr(t, "__module__", None) == "typing":
if isinstance(t, ForwardRef):
return True, value
origin = getattr(t, "__origin__")
# below is what in nicer for >= py38
# origin = get_origin(t)
# recursive validation for Unions on both types preferring the fist type
if origin is Union:
# below is what in nicer for >= py38
# t_1, t_2 = get_args(t)
args = t.__args__ # type: ignore
for arg_t in args:
t_success, t_value = _validate_type(arg_t, value)
if t_success:
return True, t_value
return False, value
if origin is dict:
if not isinstance(value, dict):
return False, value
if value == {}:
return True, value
if not getattr(t, "__args__", None):
return True, value
t_key, t_value = t.__args__ # type: ignore
if (
getattr(t_key, "__name__", None),
getattr(t_value, "__name__", None),
) == ("KT", "VT"):
return True, value
# we're only checking the first item, but the for loop and return after
# evaluating the first item is the fastest way
for dict_key, dict_value in value.items():
valid_key, _ = _validate_type(t_key, dict_key)
valid_value, _ = _validate_type(t_value, dict_value)
if valid_key and valid_value:
return True, value
return False, value
if origin is list:
if not isinstance(value, list):
return False, value
if value == []:
return True, value
if not hasattr(t, "__args__"):
return True, value
t_items = t.__args__[0] # type: ignore
if getattr(t_items, "__name__", None) == "T":
return True, value
first_item_valid, _ = _validate_type(t_items, value[0])
if first_item_valid:
return True, value
return False, value
if origin is tuple:
if not isinstance(value, tuple):
return False, value
if not hasattr(t, "__args__"):
return True, value
args = t.__args__ # type: ignore
if args == tuple():
return True, value
# we're not checking for empty tuple, cause tuple lengths must match
if len(args) != len(value):
return False, value
values = []
for t_item, v_item in zip(args, value):
item_valid, item_value = _validate_type(t_item, v_item)
if not item_valid:
return False, value
values.append(item_value)
return True, tuple(values)
if origin is set:
if not isinstance(value, set):
return False, value
if not hasattr(t, "__args__"):
return True, value
t_items = t.__args__[0] # type: ignore
first_item_valid, _ = _validate_type(t_items, next(iter(value)))
if first_item_valid:
return True, value
return False, value
if isinstance(value, t):
return True, value
with contextlib.suppress(ValueError, TypeError):
if t is float and value is not None:
return True, float(value)
# TODO: dafuq, i had to add this not list check
# but it would also fail for objects and other complex values
if t is str and value and not isinstance(value, list):
return True, str(value)
return False, value
class Base(_RegisteringBase, speckle_type="Base"):
# id: Union[str, None] = None
# totalChildrenCount: Union[int, None] = None
# applicationId: Union[str, None] = None
_units: Union[None, str] = None
def __init__(
self,
id: str | None = None,
# totalChildrenCount: Union[int, None] = None,
applicationId: str | None = None,
**kwargs,
) -> None:
self.id = id
# self.totalChildrenCount = totalChildrenCount
self.applicationId = applicationId
super().__init__()
for k, v in kwargs.items():
self.__setattr__(k, v)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(id: {self.id}, "
f"speckle_type: {self.speckle_type}, "
# f"totalChildrenCount: {self.totalChildrenCount})"
)
def __str__(self) -> str:
return self.__repr__()
@classmethod
def of_type(cls, speckle_type: str, **kwargs) -> "Base":
"""
Get a plain Base object with a specified speckle_type.
The speckle_type is protected and cannot be overwritten on a class instance.
This is to prevent problems with receiving in other platforms or connectors.
However, if you really need a base with a different type, here is a helper
to do that for you.
This is used in the deserialisation of unknown types so their speckle_type
can be preserved.
"""
b = cls(**kwargs)
b.__dict__.update(speckle_type=speckle_type)
return b
def __setitem__(self, name: str, value: Any) -> None:
self.validate_prop_name(name)
self.__dict__[name] = value
def __getitem__(self, name: str) -> Any:
return self.__dict__[name]
def __setattr__(self, name: str, value: Any) -> None:
"""
Type checking, guard attribute, and property set mechanism.
The `speckle_type` is a protected class attribute it must not be overridden.
This also performs a type check if the attribute is type hinted.
"""
if name == "speckle_type":
# not sure if we should raise an exception here??
# raise SpeckleException(
# "Cannot override the `speckle_type`. This is set manually by the class or on deserialisation"
# )
return
# if value is not None:
value = self._type_check(name, value)
attr = getattr(self.__class__, name, None)
if isinstance(attr, property):
try:
attr.__set__(self, value)
except AttributeError:
return # the prop probably doesn't have a setter
super().__setattr__(name, value)
@classmethod
def update_forward_refs(cls) -> None:
"""
Attempts to populate the internal defined types dict for type checking
sometime after defining the class.
This is already done when defining the class, but can be called
again if references to undefined types were
included.
See `objects.geometry` for an example of how this is used with
the Brep class definitions.
"""
try:
cls._attr_types = get_type_hints(cls)
except Exception as e:
warn(f"Could not update forward refs for class {cls.__name__}: {e}")
@classmethod
def validate_prop_name(cls, name: str) -> None:
"""Validator for dynamic attribute names."""
if name in {"", "@"}:
raise ValueError("Invalid Name: Base member names cannot be empty strings")
if name.startswith("@@"):
raise ValueError(
"Invalid Name: Base member names cannot start with more than one '@'",
)
if "." in name or "/" in name:
raise ValueError(
"Invalid Name: Base member names cannot contain characters '.' or '/'",
)
def _type_check(self, name: str, value: Any) -> Any:
"""
Lightweight type checking of values before setting them
NOTE: Does not check subscripted types within generics as the performance hit
of checking each item within a given collection isn't worth it.
Eg if you have a type Dict[str, float],
we will only check if the value you're trying to set is a dict.
"""
types = getattr(self, "_attr_types", {})
t = types.get(name, None)
valid, checked_value = _validate_type(t, value)
if valid:
return checked_value
raise SpeckleException(
f"Cannot set '{self.__class__.__name__}.{name}':"
f"it expects type '{str(t)}',"
f"but received type '{type(value).__name__}'"
)
def add_chunkable_attrs(self, **kwargs: int) -> None:
"""
Mark defined attributes as chunkable for serialisation
Arguments:
kwargs {int} -- the name of the attribute as the keyword
and the chunk size as the arg
"""
chunkable = {k: v for k, v in kwargs.items() if isinstance(v, int)}
self._chunkable = dict(self._chunkable, **chunkable)
def add_detachable_attrs(self, names: Set[str]) -> None:
"""
Mark defined attributes as detachable for serialisation
Arguments:
names {Set[str]} -- the names of the attributes to detach as a set of string
"""
self._detachable = self._detachable.union(names)
@property
def units(self) -> Union[str, None]:
return self._units
@units.setter
def units(self, value: Union[str, Units, None]):
"""While this property accepts any string value, geometry expects units to be specific strings (see Units enum)"""
if isinstance(value, str) or value is None:
self._units = value
elif isinstance(value, Units):
self._units = value.value
else:
raise SpeckleInvalidUnitException(
f"Unknown type {type(value)} received for units"
)
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
attr_dir = list(set(dir(self)) - REMOVE_FROM_DIR)
return [
name
for name in attr_dir
if not name.startswith("_") and not callable(getattr(self, name))
]
def get_serializable_attributes(self) -> List[str]:
"""Get the attributes that should be serialized"""
return sorted(list(set(self.get_member_names()) - self._serialize_ignore))
def get_typed_member_names(self) -> List[str]:
"""Get all of the names of the defined (typed) properties of this object"""
return list(self._attr_types.keys())
def get_dynamic_member_names(self) -> List[str]:
"""Get all of the names of the dynamic properties of this object"""
return list(set(self.__dict__.keys()) - set(self._attr_types.keys()))
def get_children_count(self) -> int:
"""Get the total count of children Base objects"""
parsed = []
return 1 + self._count_descendants(self, parsed)
def get_id(self, decompose: bool = False) -> str:
"""
Gets the id (a unique hash) of this object.
This method fully serializes the object which,
in the case of large objects (with many sub-objects), has a tangible cost.
Avoid using it!
Note: the hash of a decomposed object differs from that of a
non-decomposed object
Arguments:
decompose {bool} -- if True, will decompose the object in
the process of hashing it
Returns:
str -- the hash (id) of the fully serialized object
"""
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
serializer = BaseObjectSerializer()
if decompose:
serializer.write_transports = [MemoryTransport()]
return serializer.traverse_base(self)[0]
def _count_descendants(self, base: "Base", parsed: List) -> int:
if base in parsed:
return 0
parsed.append(base)
return sum(
self._handle_object_count(value, parsed)
for name, value in base.get_member_names()
if not name.startswith("@")
)
def _handle_object_count(self, obj: Any, parsed: List) -> int:
# pylint: disable=isinstance-second-argument-not-valid-type
count = 0
if obj is None:
return count
if isinstance(obj, "Base"):
count += 1
count += self._count_descendants(obj, parsed)
return count
elif isinstance(obj, list):
for item in obj:
if isinstance(item, "Base"):
count += 1
count += self._count_descendants(item, parsed)
else:
count += self._handle_object_count(item, parsed)
elif isinstance(obj, dict):
for _, value in obj.items():
if isinstance(value, "Base"):
count += 1
count += self._count_descendants(value, parsed)
else:
count += self._handle_object_count(value, parsed)
return count
Base.update_forward_refs()
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: Union[List[Any], None] = None
def __init__(self) -> None:
super().__init__()
self.data = []

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