Compare commits

..

12 Commits

Author SHA1 Message Date
izzy lyseggen 4f829d9908 feat(metrics): some cleanup and updates (#184)
- add metrics for client init / auth
- add server metrics
- remove incompatible server check in client
(at this point, it's been long enough that I think it's fine and will
save time on request / esp in places like blender)
2022-04-22 11:26:28 +01:00
luzpaz ac5345f528 Fix various typos (#181) 2022-04-21 17:56:11 +01:00
izzy lyseggen 1142481d89 fix(geometry): int(index vals) for curve encoding (#183)
* fix(geometry): int(index vals) for curve encoding

* fix(client): update poss invalid token check

server now returns `None` instead of a `GraphqlExcetion` when asking for
the user with an invalid token (or no scopes token)
2022-04-21 17:50:43 +01:00
izzy lyseggen b4690f082f feat(objects): revit params in objects for blender (#179) 2022-04-01 11:58:49 +01:00
izzy lyseggen 81a98ea938 feat(client): stream and user activity (#176)
* feat(models): `ActivityCollection` and `Activity`

* feat(client): stream activity method

* test(client): test for stream activity

* refactor(client): use datetime args for activity

* docs(client): clean up stream activity docstring

* feat/test(client): user activity
2022-03-23 17:34:10 +00:00
izzy lyseggen 9b387da77a feat(serialisation): enums (#175)
note that this won't re-serialise dynamic members as enums.
they will come back as ints for consistency w sharp

closes 🃏 Enum serialisation bug #174
2022-03-23 11:49:16 +00:00
izzy lyseggen d0724c7d06 fix(objects): brep display val fix (#170)
* fix(client): auth fix

* fix(objects): temp displayValue prop setter

will be removed in the future, but keeping it now for backwards compat
2022-03-02 14:17:04 +00:00
izzy lyseggen 1414a3611b fix(wrapper): use full url for creating shell account (#169)
used for creating a transport if you don't have a local account
for the specified server
2022-03-01 10:33:47 +00:00
izzy lyseggen a553c17c43 fix/test(serialization): null values in dicts (#168) 2022-02-24 11:31:04 +00:00
izzy lyseggen 0be3fac6ab docs: update streamwrapper docstring 2022-02-23 16:52:23 +00:00
izzy lyseggen 944e70221e refactor(auth&metrics): use accounts everywhere and switch metrics (#166)
* feat(metrics): wip

* refactor(auth): use accts instead of tokens

* fix(wrapper): delayed auth bug

* refactor(memory): quick fix

* fix(creds): change incompatible py 3.8+ syntax

* feat(anatylics): updates tracking

* fix(credentials): catch when no accts

* fix(metrics): remove unused field

* feat(wrapper): raise exception for old import

* feat(analytics): consolidate names
2022-02-23 11:00:04 +00:00
izzy lyseggen 21f13c4750 chore: lgtm fixes (#164)
* chore: clean imports

* chore: more lil fixes
2022-02-21 11:19:06 +00:00
40 changed files with 1158 additions and 521 deletions
Generated
+214 -15
View File
@@ -79,6 +79,22 @@ typing-extensions = ">=3.7.4"
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.3.2)", "aiohttp-cors"]
[[package]]
name = "botocore"
version = "1.24.4"
description = "Low-level, data-driven core of boto 3."
category = "main"
optional = false
python-versions = ">= 3.6"
[package.dependencies]
jmespath = ">=0.7.1,<1.0.0"
python-dateutil = ">=2.1,<3.0.0"
urllib3 = ">=1.25.4,<1.27"
[package.extras]
crt = ["awscrt (==0.12.5)"]
[[package]]
name = "certifi"
version = "2021.10.8"
@@ -148,35 +164,54 @@ category = "main"
optional = false
python-versions = ">=3.6, <3.7"
[[package]]
name = "deprecated"
version = "1.2.13"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
wrapt = ">=1.10,<2"
[package.extras]
dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"]
[[package]]
name = "gql"
version = "3.0.0b1"
version = "3.0.0"
description = "GraphQL client for Python"
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
aiohttp = {version = ">=3.7.1,<3.8.0", optional = true, markers = "extra == \"all\""}
graphql-core = ">=3.1.5,<3.2"
aiohttp = {version = ">=3.7.1,<3.9.0", optional = true, markers = "extra == \"all\""}
botocore = {version = ">=1.21,<2", optional = true, markers = "extra == \"all\""}
graphql-core = ">=3.2,<3.3"
requests = {version = ">=2.26,<3", optional = true, markers = "extra == \"all\""}
requests_toolbelt = {version = ">=0.9.1,<1", optional = true, markers = "extra == \"all\""}
urllib3 = {version = ">=1.26", optional = true, markers = "extra == \"all\""}
websockets = {version = ">=9,<10", optional = true, markers = "extra == \"all\""}
websockets = [
{version = ">=9,<10", optional = true, markers = "python_version <= \"3.6\" or python_version <= \"3.6\" and extra == \"all\""},
{version = ">=10,<11", optional = true, markers = "python_version > \"3.6\" or python_version > \"3.6\" and extra == \"all\""},
]
yarl = ">=1.6,<2.0"
[package.extras]
aiohttp = ["aiohttp (>=3.7.1,<3.8.0)"]
all = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "websockets (>=9,<10)"]
dev = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "websockets (>=9,<10)", "black (==19.10b0)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mypy (==0.770)", "sphinx (>=3.0.0,<4)", "sphinx_rtd_theme (>=0.4,<1)", "sphinx-argparse (==0.2.5)", "parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
aiohttp = ["aiohttp (>=3.7.1,<3.9.0)"]
all = ["aiohttp (>=3.7.1,<3.9.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "botocore (>=1.21,<2)", "websockets (>=9,<10)", "websockets (>=10,<11)"]
botocore = ["botocore (>=1.21,<2)"]
dev = ["aiohttp (>=3.7.1,<3.9.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "botocore (>=1.21,<2)", "black (==19.10b0)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "isort (==4.3.21)", "mypy (==0.910)", "sphinx (>=3.0.0,<4)", "sphinx_rtd_theme (>=0.4,<1)", "sphinx-argparse (==0.2.5)", "types-aiofiles", "types-mock", "types-requests", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-cov (==3.0.0)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles", "websockets (>=9,<10)", "websockets (>=10,<11)"]
requests = ["requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)"]
test = ["aiohttp (>=3.7.1,<3.8.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "websockets (>=9,<10)", "parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
test_no_transport = ["parse (==1.15.0)", "pytest (==5.4.2)", "pytest-asyncio (==0.11.0)", "pytest-cov (==2.8.1)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
websockets = ["websockets (>=9,<10)"]
test = ["aiohttp (>=3.7.1,<3.9.0)", "requests (>=2.26,<3)", "requests_toolbelt (>=0.9.1,<1)", "urllib3 (>=1.26)", "botocore (>=1.21,<2)", "parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-cov (==3.0.0)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles", "websockets (>=9,<10)", "websockets (>=10,<11)"]
test_no_transport = ["parse (==1.15.0)", "pytest (==6.2.5)", "pytest-asyncio (==0.16.0)", "pytest-cov (==3.0.0)", "mock (==4.0.2)", "vcrpy (==4.0.2)", "aiofiles"]
websockets = ["websockets (>=9,<10)", "websockets (>=10,<11)"]
[[package]]
name = "graphql-core"
version = "3.1.6"
version = "3.2.0"
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
category = "main"
optional = false
@@ -240,6 +275,14 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"]
colors = ["colorama (>=0.4.3,<0.5.0)"]
plugins = ["setuptools"]
[[package]]
name = "jmespath"
version = "0.10.0"
description = "JSON Matching Expressions"
category = "main"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "multidict"
version = "5.2.0"
@@ -373,6 +416,17 @@ python-versions = "*"
[package.dependencies]
pytest = "*"
[[package]]
name = "python-dateutil"
version = "2.8.2"
description = "Extensions to the standard Python datetime module"
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
[package.dependencies]
six = ">=1.5"
[[package]]
name = "regex"
version = "2021.11.10"
@@ -410,6 +464,14 @@ python-versions = "*"
[package.dependencies]
requests = ">=2.0.1,<3.0.0"
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "toml"
version = "0.10.2"
@@ -471,6 +533,22 @@ category = "main"
optional = false
python-versions = ">=3.6.1"
[[package]]
name = "websockets"
version = "10.2"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "wrapt"
version = "1.13.3"
description = "Module for decorators, wrappers and monkey patching."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
[[package]]
name = "yarl"
version = "1.7.2"
@@ -499,7 +577,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.6.5"
content-hash = "842f390b7c3b4c3a21d2113dc0aec26707b7698d748cbf1a195b9f4e29f40072"
content-hash = "78b3a6fe933735ef4f3c0dca34c08521ab2c11aceb2b7f5572846870d39d46f4"
[metadata.files]
aiohttp = [
@@ -560,6 +638,10 @@ attrs = [
black = [
{file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"},
]
botocore = [
{file = "botocore-1.24.4-py3-none-any.whl", hash = "sha256:692fcad31733674c8faffc65da8ddc8ab4839f407ad8470c103e0e7aed9c7556"},
{file = "botocore-1.24.4.tar.gz", hash = "sha256:8e9ae6b718ca318b4aeeee45aa9fad5f52e882a2291d919cfa4b753928223abb"},
]
certifi = [
{file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
{file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
@@ -633,12 +715,16 @@ dataclasses = [
{file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"},
{file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"},
]
deprecated = [
{file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"},
{file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"},
]
gql = [
{file = "gql-3.0.0b1.tar.gz", hash = "sha256:a08195e73ed08785e1aef71a540e514f80dbfa711b9effba31ba15fcb687f1fd"},
{file = "gql-3.0.0.tar.gz", hash = "sha256:a21ff880a69caec8786be5cc6de7de6fc2f2b87547af8cd30746b9401ffd47b2"},
]
graphql-core = [
{file = "graphql-core-3.1.6.tar.gz", hash = "sha256:e65975b6a13878f9113a1fa5320760585b522d139944e005936b1b8358d0651a"},
{file = "graphql_core-3.1.6-py3-none-any.whl", hash = "sha256:c78d09596d347e1cffd266c5384abfedf43ed1eae08729773bebb3d527fe5a14"},
{file = "graphql-core-3.2.0.tar.gz", hash = "sha256:86e2a0be008bfde19ef78388de8a725a1d942a9190ca431c24a60837973803ce"},
{file = "graphql_core-3.2.0-py3-none-any.whl", hash = "sha256:0dda7e63676f119bb3d814621190fedad72fda07a8e9ab780bedd9f1957c6dc6"},
]
idna = [
{file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
@@ -659,6 +745,10 @@ isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
jmespath = [
{file = "jmespath-0.10.0-py2.py3-none-any.whl", hash = "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"},
{file = "jmespath-0.10.0.tar.gz", hash = "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9"},
]
multidict = [
{file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"},
{file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"},
@@ -794,6 +884,10 @@ pytest-ordering = [
{file = "pytest_ordering-0.6-py2-none-any.whl", hash = "sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2"},
{file = "pytest_ordering-0.6-py3-none-any.whl", hash = "sha256:3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6"},
]
python-dateutil = [
{file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
{file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
]
regex = [
{file = "regex-2021.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9345b6f7ee578bad8e475129ed40123d265464c4cfead6c261fd60fc9de00bcf"},
{file = "regex-2021.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:416c5f1a188c91e3eb41e9c8787288e707f7d2ebe66e0a6563af280d9b68478f"},
@@ -878,6 +972,10 @@ requests-toolbelt = [
{file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
{file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
]
six = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
@@ -995,6 +1093,107 @@ websockets = [
{file = "websockets-9.1-cp39-cp39-win32.whl", hash = "sha256:be5fd35e99970518547edc906efab29afd392319f020c3c58b0e1a158e16ed20"},
{file = "websockets-9.1-cp39-cp39-win_amd64.whl", hash = "sha256:85db8090ba94e22d964498a47fdd933b8875a1add6ebc514c7ac8703eb97bbf0"},
{file = "websockets-9.1.tar.gz", hash = "sha256:276d2339ebf0df4f45df453923ebd2270b87900eda5dfd4a6b0cfa15f82111c3"},
{file = "websockets-10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5396710f86a306cf52f87fd8ea594a0e894ba0cc5a36059eaca3a477dc332aa"},
{file = "websockets-10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b22bdc795e62e71118b63e14a08bacfa4f262fd2877de7e5b950f5ac16b0348f"},
{file = "websockets-10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b04270b5613f245ec84bb2c6a482a9d009aefad37c0575f6cda8499125d5d5c"},
{file = "websockets-10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5c335dc0e7dc271ef36df3f439868b3c790775f345338c2f61a562f1074187b"},
{file = "websockets-10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a009eb551c46fd79737791c0c833fc0e5b56bcd1c3057498b262d660b92e9cd"},
{file = "websockets-10.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a10c0c1ee02164246f90053273a42d72a3b2452a7e7486fdae781138cf7fbe2d"},
{file = "websockets-10.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7b38a5c9112e3dbbe45540f7b60c5204f49b3cb501b40950d6ab34cd202ab1d0"},
{file = "websockets-10.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2aa9b91347ecd0412683f28aabe27f6bad502d89bd363b76e0a3508b1596402e"},
{file = "websockets-10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b7fe45ae43ac814beb8ca09d6995b56800676f2cfa8e23f42839dc69bba34a42"},
{file = "websockets-10.2-cp310-cp310-win32.whl", hash = "sha256:cef40a1b183dcf39d23b392e9dd1d9b07ab9c46aadf294fff1350fb79146e72b"},
{file = "websockets-10.2-cp310-cp310-win_amd64.whl", hash = "sha256:c21a67ab9a94bd53e10bba21912556027fea944648a09e6508415ad14e37c325"},
{file = "websockets-10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb316b87cbe3c0791c2ad92a5a36bf6adc87c457654335810b25048c1daa6fd5"},
{file = "websockets-10.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f14bd10e170abc01682a9f8b28b16e6f20acf6175945ef38db6ffe31b0c72c3f"},
{file = "websockets-10.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fa35c5d1830d0fb7b810324e9eeab9aa92e8f273f11fdbdc0741dcded6d72b9f"},
{file = "websockets-10.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:71a4491cfe7a9f18ee57d41163cb6a8a3fa591e0f0564ca8b0ed86b2a30cced4"},
{file = "websockets-10.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6193bbc1ee63aadeb9a4d81de0e19477401d150d506aee772d8380943f118186"},
{file = "websockets-10.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8beac786a388bb99a66c3be4ab0fb38273c0e3bc17f612a4e0a47c4fc8b9c045"},
{file = "websockets-10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c67d9cacb3f6537ca21e9b224d4fd08481538e43bcac08b3d93181b0816def39"},
{file = "websockets-10.2-cp37-cp37m-win32.whl", hash = "sha256:a03a25d95cc7400bd4d61a63460b5d85a7761c12075ee2f51de1ffe73aa593d3"},
{file = "websockets-10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:f8296b8408ec6853b26771599990721a26403e62b9de7e50ac0a056772ac0b5e"},
{file = "websockets-10.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7bb9d8a6beca478c7e9bdde0159bd810cc1006ad6a7cb460533bae39da692ca2"},
{file = "websockets-10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:05f6e9757017270e7a92a2975e2ae88a9a582ffc4629086fd6039aa80e99cd86"},
{file = "websockets-10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1c9031e90ebfc486e9cdad532b94004ade3aa39a31d3c46c105bb0b579cd2490"},
{file = "websockets-10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82bc33db6d8309dc27a3bee11f7da2288ad925fcbabc2a4bb78f7e9c56249baf"},
{file = "websockets-10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:24b879ba7db12bb525d4e58089fcbe6a3df3ce4666523183654170e86d372cbe"},
{file = "websockets-10.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cf931c33db9c87c53d009856045dd524e4a378445693382a920fa1e0eb77c36c"},
{file = "websockets-10.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:669e54228a4d9457abafed27cbf0e2b9f401445c4dfefc12bf8e4db9751703b8"},
{file = "websockets-10.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bffc65442dd35c473ca9790a3fa3ba06396102a950794f536783f4b8060af8dd"},
{file = "websockets-10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d4d110a84b63c5cfdd22485acc97b8b919aefeecd6300c0c9d551e055b9a88ea"},
{file = "websockets-10.2-cp38-cp38-win32.whl", hash = "sha256:117383d0a17a0dda349f7a8790763dde75c1508ff8e4d6e8328b898b7df48397"},
{file = "websockets-10.2-cp38-cp38-win_amd64.whl", hash = "sha256:0b66421f9f13d4df60cd48ab977ed2c2b6c9147ae1a33caf5a9f46294422fda1"},
{file = "websockets-10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac081aa0307f263d63c5ff0727935c736c8dad51ddf2dc9f5d0c4759842aefaa"},
{file = "websockets-10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b4059e2ccbe6587b6dc9a01db5fc49ead9a884faa4076eea96c5ec62cb32f42a"},
{file = "websockets-10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9ca2ca05a4c29179f06cf6727b45dba5d228da62623ec9df4184413d8aae6cb9"},
{file = "websockets-10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97950c7c844ec6f8d292440953ae18b99e3a6a09885e09d20d5e7ecd9b914cf8"},
{file = "websockets-10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:98f57b3120f8331cd7440dbe0e776474f5e3632fdaa474af1f6b754955a47d71"},
{file = "websockets-10.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a72b92f96e5e540d5dda99ee3346e199ade8df63152fa3c737260da1730c411f"},
{file = "websockets-10.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:038afef2a05893578d10dadbdbb5f112bd115c46347e1efe99f6a356ff062138"},
{file = "websockets-10.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f09f46b1ff6d09b01c7816c50bd1903cf7d02ebbdb63726132717c2fcda835d5"},
{file = "websockets-10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2349fa81b6b959484bb2bda556ccb9eb70ba68987646a0f8a537a1a18319fb03"},
{file = "websockets-10.2-cp39-cp39-win32.whl", hash = "sha256:bef03a51f9657fb03d8da6ccd233fe96e04101a852f0ffd35f5b725b28221ff3"},
{file = "websockets-10.2-cp39-cp39-win_amd64.whl", hash = "sha256:1c1f3b18c8162e3b09761d0c6a0305fd642934202541cc511ef972cb9463261e"},
{file = "websockets-10.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5a38a0175ae82e4a8c4bac29fc01b9ee26d7d5a614e5ee11e7813c68a7d938ce"},
{file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e56606842bb24e16e36ae7eb308d866b4249cf0be8f63b212f287eeb76b124"},
{file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0f73cb2526d6da268e86977b2c4b58f2195994e53070fe567d5487c6436047e6"},
{file = "websockets-10.2-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cd02f36d37e503aca88ab23cc0a1a0e92a263d37acf6331521eb38040dcf77b"},
{file = "websockets-10.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:56d48eebe9e39ce0d68701bce3b21df923aa05dcc00f9fd8300de1df31a7c07c"},
{file = "websockets-10.2.tar.gz", hash = "sha256:8351c3c86b08156337b0e4ece0e3c5ec3e01fcd14e8950996832a23c99416098"},
]
wrapt = [
{file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"},
{file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"},
{file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"},
{file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"},
{file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"},
{file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"},
{file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"},
{file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"},
{file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"},
{file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"},
{file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"},
{file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"},
{file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"},
{file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"},
{file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"},
{file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"},
{file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"},
{file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"},
{file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"},
{file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"},
{file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"},
{file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"},
{file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"},
{file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"},
{file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"},
{file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"},
{file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"},
{file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"},
{file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"},
{file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"},
{file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"},
{file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"},
{file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"},
{file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"},
{file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"},
{file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"},
{file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"},
{file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"},
{file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"},
{file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"},
{file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"},
{file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"},
{file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"},
{file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"},
{file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"},
{file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"},
{file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"},
{file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"},
{file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"},
{file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"},
{file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"},
]
yarl = [
{file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"},
+1
View File
@@ -16,6 +16,7 @@ pydantic = "^1.8.2"
appdirs = "^1.4.4"
gql = {version = ">=3.0.0b1", extras = ["all"], allow-prereleases = true}
ujson = "^4.3.0"
Deprecated = "^1.2.13"
[tool.poetry.dev-dependencies]
black = "^20.8b1"
+67 -30
View File
@@ -1,7 +1,9 @@
import re
from warnings import warn
from deprecated import deprecated
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.logging import metrics
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
SpeckleWarning,
)
@@ -39,9 +41,9 @@ class SpeckleClient:
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with a token (account has been added in Speckle Manager)
# authenticate the client with an account (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate(token=account.token)
client.authenticate_with_account(account)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
@@ -55,6 +57,7 @@ class SpeckleClient:
USE_SSL = True
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
metrics.track(metrics.CLIENT, custom_props={"name": "create"})
ws_protocol = "ws"
http_protocol = "http"
@@ -66,9 +69,9 @@ class SpeckleClient:
host = re.sub(r"((^\w+:|^)\/\/)|(\/$)", "", host)
self.url = f"{http_protocol}://{host}"
self.graphql = self.url + "/graphql"
self.graphql = f"{self.url}/graphql"
self.ws_url = f"{ws_protocol}://{host}/graphql"
self.me = None
self.account = Account()
self.httpclient = Client(
transport=RequestsHTTPTransport(url=self.graphql, verify=True, retries=3)
@@ -77,21 +80,25 @@ class SpeckleClient:
self._init_resources()
# Check compatibility with the server
try:
serverInfo = self.server.get()
if isinstance(serverInfo, Exception):
raise serverInfo
if not isinstance(serverInfo, ServerInfo):
raise Exception("Couldn't get ServerInfo")
except Exception as ex:
raise SpeckleException(f"{self.url} is not a compatible Speckle Server", ex)
# ? Check compatibility with the server - i think we can skip this at this point? save a request
# try:
# server_info = self.server.get()
# if isinstance(server_info, Exception):
# raise server_info
# if not isinstance(server_info, ServerInfo):
# raise Exception("Couldn't get ServerInfo")
# except Exception as ex:
# raise SpeckleException(
# f"{self.url} is not a compatible Speckle Server", ex
# ) from ex
def __repr__(self):
return (
f"SpeckleClient( server: {self.url}, authenticated: {self.me is not None} )"
)
return f"SpeckleClient( server: {self.url}, authenticated: {self.account.token is not None} )"
@deprecated(
version="2.6.0",
reason="Renamed: please use `authenticate_with_account` or `authenticate_with_token` instead.",
)
def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL entrypoint is created
@@ -99,9 +106,35 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.me = {"token": token}
self.authenticate_with_token(token)
self._set_up_client()
def authenticate_with_token(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL entrypoint is created
Arguments:
token {str} -- an api token
"""
self.account = get_account_from_token(token, self.url)
metrics.track(metrics.CLIENT, self.account, {"name": "authenticate with token"})
self._set_up_client()
def authenticate_with_account(self, account: Account) -> None:
"""Authenticate the client using an Account object
The account is saved in the client object and a synchronous GraphQL entrypoint is created
Arguments:
account {Account} -- the account object which can be found with `get_default_account` or `get_local_accounts`
"""
metrics.track(metrics.CLIENT, account, {"name": "authenticate with account"})
self.account = account
self._set_up_client()
def _set_up_client(self) -> None:
metrics.track(metrics.CLIENT, self.account, {"name": "set up client"})
headers = {
"Authorization": f"Bearer {self.me['token']}",
"Authorization": f"Bearer {self.account.token}",
"Content-Type": "application/json",
}
httptransport = RequestsHTTPTransport(
@@ -109,17 +142,17 @@ class SpeckleClient:
)
wstransport = WebsocketsTransport(
url=self.ws_url,
init_payload={"Authorization": f"Bearer {self.me['token']}"},
init_payload={"Authorization": f"Bearer {self.account.token}"},
)
self.httpclient = Client(transport=httptransport)
self.wsclient = Client(transport=wstransport)
self._init_resources()
if isinstance(self.user.get(), GraphQLException):
if self.user.get() is None:
warn(
SpeckleWarning(
f"Invalid token - could not authenticate Speckle Client for server {self.url}"
f"Possibly invalid token - could not authenticate Speckle Client for server {self.url}"
)
)
@@ -128,23 +161,25 @@ class SpeckleClient:
def _init_resources(self) -> None:
self.stream = stream.Resource(
me=self.me, basepath=self.url, client=self.httpclient
account=self.account, basepath=self.url, client=self.httpclient
)
self.commit = commit.Resource(
me=self.me, basepath=self.url, client=self.httpclient
account=self.account, basepath=self.url, client=self.httpclient
)
self.branch = branch.Resource(
me=self.me, basepath=self.url, client=self.httpclient
account=self.account, basepath=self.url, client=self.httpclient
)
self.object = object.Resource(
me=self.me, basepath=self.url, client=self.httpclient
account=self.account, basepath=self.url, client=self.httpclient
)
self.server = server.Resource(
me=self.me, basepath=self.url, client=self.httpclient
account=self.account, basepath=self.url, client=self.httpclient
)
self.user = user.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.user = user.Resource(me=self.me, basepath=self.url, client=self.httpclient)
self.subscribe = subscriptions.Resource(
me=self.me,
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
)
@@ -152,7 +187,9 @@ class SpeckleClient:
def __getattr__(self, name):
try:
attr = getattr(resources, name)
return attr.Resource(me=self.me, basepath=self.url, client=self.httpclient)
return attr.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
except:
raise SpeckleException(
f"Method {name} is not supported by the SpeckleClient class"
+49 -148
View File
@@ -1,29 +1,25 @@
import os
from warnings import warn
from pydantic import BaseModel
from pydantic import BaseModel, Field
from typing import List, Optional
from urllib.parse import urlparse, unquote
from specklepy.logging import metrics
from specklepy.api.models import ServerInfo
from specklepy.api.client import SpeckleClient
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.transports.server.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.logging.exceptions import SpeckleException
class UserInfo(BaseModel):
name: str
email: str
name: Optional[str]
email: Optional[str]
company: Optional[str]
id: str
id: Optional[str]
class Account(BaseModel):
isDefault: bool = None
token: str
isDefault: bool = False
token: str = None
refreshToken: str = None
serverInfo: ServerInfo
userInfo: UserInfo
serverInfo: ServerInfo = Field(default_factory=ServerInfo)
userInfo: UserInfo = Field(default_factory=UserInfo)
id: str = None
def __repr__(self) -> str:
@@ -32,6 +28,12 @@ class Account(BaseModel):
def __str__(self) -> str:
return self.__repr__()
@classmethod
def from_token(cls, token: str, server_url: str = None):
acct = cls(token=token)
acct.serverInfo.url = server_url
return acct
def get_local_accounts(base_path: str = None) -> List[Account]:
"""Gets all the accounts present in this environment
@@ -42,7 +44,6 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
Returns:
List[Account] -- list of all local accounts or an empty list if no accounts were found
"""
metrics.track(metrics.ACCOUNT_LIST)
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
json_path = os.path.join(account_storage._base_path, "Accounts")
os.makedirs(json_path, exist_ok=True)
@@ -63,6 +64,13 @@ def get_local_accounts(base_path: str = None) -> List[Account]:
"Invalid json accounts could not be read. Please fix or remove them.",
ex,
)
metrics.track(
metrics.ACCOUNTS,
next(
(acc for acc in accounts if acc.isDefault),
accounts[0] if accounts else None,
),
)
return accounts
@@ -75,7 +83,6 @@ def get_default_account(base_path: str = None) -> Account:
Returns:
Account -- the default account or None if no local accounts were found
"""
metrics.track(metrics.ACCOUNT_DEFAULT)
accounts = get_local_accounts(base_path=base_path)
if not accounts:
return None
@@ -84,147 +91,41 @@ def get_default_account(base_path: str = None) -> Account:
if not default:
default = accounts[0]
default.isDefault = True
metrics.initialise_tracker(default)
return default
class StreamWrapper:
def get_account_from_token(token: str, server_url: str = None) -> Account:
"""Gets the local account for the token if it exists
Arguments:
token {str} -- the api token
Returns:
Account -- the local account with this token or a shell account containing just the token and url if no local account is found
"""
The `StreamWrapper` gives you some handy helpers to deal with urls and get authenticated clients and transports.
accounts = get_local_accounts()
if not accounts:
return Account.from_token(token, server_url)
Construct a `StreamWrapper` with a stream, branch, commit, or object URL. The corresponding ids will be stored
in the wrapper. If you have local accounts on the machine, you can use the `get_account` and `get_client` methods
to get a local account for the server. You can also pass a token into `get_client` if you don't have a corresponding
local account for the server.
acct = next((acc for acc in accounts if acc.token == token), None)
if acct:
return acct
```py
from specklepy.api.credentials import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
# get an authenticated ServerTransport if you have a local account for the server
transport = wrapper.get_transport()
```
"""
stream_url: str = None
use_ssl: bool = True
host: str = None
stream_id: str = None
commit_id: str = None
object_id: str = None
branch_name: str = None
_client: SpeckleClient = None
_account: Account = None
def __repr__(self):
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
def __str__(self) -> str:
return self.__repr__()
@property
def type(self) -> str:
if self.object_id:
return "object"
elif self.commit_id:
return "commit"
elif self.branch_name:
return "branch"
else:
return "stream" if self.stream_id else "invalid"
def __init__(self, url: str) -> None:
metrics.track("streamwrapper")
self.stream_url = url
parsed = urlparse(url)
self.host = parsed.netloc
self.use_ssl = parsed.scheme == "https"
segments = parsed.path.strip("/").split("/", 3)
if not segments or len(segments) < 2:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
while segments:
segment = segments.pop(0)
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
def get_account(self) -> Account:
"""
Gets an account object for this server from the local accounts db (added via Speckle Manager or a json file)
"""
if self._account:
return self._account
self._account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
None,
if server_url:
url = server_url.lower()
acct = next(
(acc for acc in accounts if url in acc.serverInfo.url.lower()), None
)
if acct:
return acct
return self._account
return Account.from_token(token, server_url)
def get_client(self, token: str = None) -> SpeckleClient:
"""
Gets an authenticated client for this server. You may provide a token if there aren't any local accounts on this
machine. If no account is found and no token is provided, an unauthenticated client is returned.
Arguments:
token {str} -- optional token if no local account is available (defaults to None)
Returns:
SpeckleClient -- authenticated with a corresponding local account or the provided token
"""
if self._client and token is None:
return self._client
if not self._account:
self.get_account()
if not self._client:
self._client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if self._account is None and token is None:
warn(f"No local account found for server {self.host}", SpeckleWarning)
return self._client
self._client.authenticate(self._account.token if self._account else token)
return self._client
def get_transport(self, token: str = None) -> ServerTransport:
"""
Gets a server transport for this stream using an authenticated client. If there is no local account for this
server and the client was not authenticated with a token, this will throw an exception.
Returns:
ServerTransport -- constructed for this stream with a pre-authenticated client
"""
if not self._client or not self._client.me:
self.get_client(token)
return ServerTransport(self.stream_id, self._client)
class StreamWrapper:
def __init__(self, url: str = None) -> None:
raise SpeckleException(
message="The StreamWrapper has moved as of v2.6.0! Please import from specklepy.api.wrapper",
exception=DeprecationWarning,
)
+34 -5
View File
@@ -23,7 +23,7 @@ class Commit(BaseModel):
authorId: Optional[str]
authorAvatar: Optional[str]
branchName: Optional[str]
createdAt: Optional[str]
createdAt: Optional[datetime]
sourceApplication: Optional[str]
referencedObject: Optional[str]
totalChildrenCount: Optional[int]
@@ -38,7 +38,7 @@ class Commit(BaseModel):
class Commits(BaseModel):
totalCount: Optional[int]
cursor: Optional[Any]
cursor: Optional[datetime]
items: List[Commit] = []
@@ -47,7 +47,7 @@ class Object(BaseModel):
speckleType: Optional[str]
applicationId: Optional[str]
totalChildrenCount: Optional[int]
createdAt: Optional[str]
createdAt: Optional[datetime]
class Branch(BaseModel):
@@ -68,8 +68,8 @@ class Stream(BaseModel):
name: Optional[str]
description: Optional[str]
isPublic: Optional[bool]
createdAt: Optional[str]
updatedAt: Optional[str]
createdAt: Optional[datetime]
updatedAt: Optional[datetime]
collaborators: List[Collaborator] = []
branches: Optional[Branches]
commit: Optional[Commit]
@@ -106,6 +106,35 @@ class User(BaseModel):
return self.__repr__()
class Activity(BaseModel):
actionType: Optional[str]
info: Optional[dict]
userId: Optional[str]
streamId: Optional[str]
resourceId: Optional[str]
resourceType: Optional[str]
message: Optional[str]
time: Optional[datetime]
def __repr__(self) -> str:
return f"Activity( streamId: {self.streamId}, actionType: {self.actionType}, message: {self.message}, userId: {self.userId} )"
def __str__(self) -> str:
return self.__repr__()
class ActivityCollection(BaseModel):
totalCount: Optional[int]
items: Optional[List[Activity]]
cursor: Optional[datetime]
def __repr__(self) -> str:
return f"ActivityCollection( totalCount: {self.totalCount}, items: {len(self.items) if self.items else 0}, cursor: {self.cursor.isoformat()} )"
def __str__(self) -> str:
return self.__repr__()
class ServerInfo(BaseModel):
name: Optional[str]
company: Optional[str]
+14 -6
View File
@@ -2,6 +2,7 @@ from typing import List
from specklepy.logging import metrics
from specklepy.objects.base import Base
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.transports.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
@@ -9,7 +10,7 @@ from specklepy.serialization.base_object_serializer import BaseObjectSerializer
def send(
base: Base,
transports: List[AbstractTransport] = [],
transports: List[AbstractTransport] = None,
use_default_cache: bool = True,
):
"""Sends an object via the provided transports. Defaults to the local cache.
@@ -22,11 +23,18 @@ def send(
Returns:
str -- the object id of the sent object
"""
metrics.track(metrics.SEND)
if not transports and not use_default_cache:
raise SpeckleException(
message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
)
if transports is None:
metrics.track(metrics.SEND)
transports = []
else:
metrics.track(metrics.SEND, getattr(transports[0], "account", None))
if use_default_cache:
transports.insert(0, SQLiteTransport())
@@ -34,12 +42,12 @@ def send(
for t in transports:
t.begin_write()
hash, _ = serializer.write_json(base=base)
obj_hash, _ = serializer.write_json(base=base)
for t in transports:
t.end_write()
return hash
return obj_hash
def receive(
@@ -58,13 +66,13 @@ def receive(
Returns:
Base -- the base object
"""
metrics.track(metrics.RECEIVE)
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
if not local_transport:
local_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=local_transport)
# try local transport first. if the parent is there, we assume all the children are there and continue wth deserialisation using the local transport
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialisation using the local transport
obj_string = local_transport.get_object(obj_id)
if obj_string:
return serializer.read_json(obj_string=obj_string)
+3 -2
View File
@@ -1,3 +1,4 @@
from specklepy.api.credentials import Account
from specklepy.transports.sqlite import SQLiteTransport
from typing import Dict, List
from gql.client import Client
@@ -10,13 +11,13 @@ from specklepy.serialization.base_object_serializer import BaseObjectSerializer
class ResourceBase(object):
def __init__(
self,
me: Dict,
account: Account,
basepath: str,
client: Client,
name: str,
methods: list,
) -> None:
self.me = me
self.account = account
self.basepath = basepath
self.client = client
self.name = name
+13 -9
View File
@@ -1,9 +1,7 @@
from specklepy.api.resources import stream
from typing import List, Optional
from gql import gql
from pydantic.main import BaseModel
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Branch
from specklepy.logging import metrics
NAME = "branch"
METHODS = ["create"]
@@ -12,9 +10,13 @@ METHODS = ["create"]
class Resource(ResourceBase):
"""API Access class for branches"""
def __init__(self, me, basepath, client) -> None:
def __init__(self, account, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
self.schema = Branch
@@ -30,7 +32,7 @@ class Resource(ResourceBase):
Returns:
id {str} -- the newly created branch's id
"""
metrics.track(metrics.BRANCH, self.account, {"name": "create"})
query = gql(
"""
mutation BranchCreate($branch: BranchCreateInput!) {
@@ -61,7 +63,7 @@ class Resource(ResourceBase):
Returns:
Branch -- the fetched branch with its latest commits
"""
metrics.track(metrics.BRANCH, self.account, {"name": "get"})
query = gql(
"""
query BranchGet($stream_id: String!, $name: String!, $commits_limit: Int!) {
@@ -109,6 +111,7 @@ class Resource(ResourceBase):
Returns:
List[Branch] -- the branches on the stream
"""
metrics.track(metrics.BRANCH, self.account, {"name": "get"})
query = gql(
"""
query BranchesGet($stream_id: String!, $branches_limit: Int!, $commits_limit: Int!) {
@@ -161,8 +164,9 @@ class Resource(ResourceBase):
description {str} -- optional: the updated branch description
Returns:
bool -- True if update is successfull
bool -- True if update is successful
"""
metrics.track(metrics.BRANCH, self.account, {"name": "update"})
query = gql(
"""
mutation BranchUpdate($branch: BranchUpdateInput!) {
@@ -196,7 +200,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if deletion is successful
"""
metrics.track(metrics.BRANCH, self.account, {"name": "delete"})
query = gql(
"""
mutation BranchDelete($branch: BranchDeleteInput!) {
+12 -2
View File
@@ -2,6 +2,7 @@ from typing import Optional, List
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Commit
from specklepy.logging import metrics
NAME = "commit"
@@ -11,9 +12,13 @@ METHODS = []
class Resource(ResourceBase):
"""API Access class for commits"""
def __init__(self, me, basepath, client) -> None:
def __init__(self, account, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
self.schema = Commit
@@ -66,6 +71,7 @@ class Resource(ResourceBase):
Returns:
List[Commit] -- a list of the most recent commit objects
"""
metrics.track(metrics.COMMIT, self.account, {"name": "get"})
query = gql(
"""
query Commits($stream_id: String!, $limit: Int!) {
@@ -119,6 +125,7 @@ class Resource(ResourceBase):
Returns:
str -- the id of the created commit
"""
metrics.track(metrics.COMMIT, self.account, {"name": "create"})
query = gql(
"""
mutation CommitCreate ($commit: CommitCreateInput!){ commitCreate(commit: $commit)}
@@ -152,6 +159,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if the operation succeeded
"""
metrics.track(metrics.COMMIT, self.account, {"name": "update"})
query = gql(
"""
mutation CommitUpdate($commit: CommitUpdateInput!){ commitUpdate(commit: $commit)}
@@ -176,6 +184,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if the operation succeeded
"""
metrics.track(metrics.COMMIT, self.account, {"name": "delete"})
query = gql(
"""
mutation CommitDelete($commit: CommitDeleteInput!){ commitDelete(commit: $commit)}
@@ -197,6 +206,7 @@ class Resource(ResourceBase):
"""
Mark a commit object a received by the source application.
"""
metrics.track(metrics.COMMIT, self.account, {"name": "received"})
query = gql(
"""
mutation CommitReceive($receivedInput:CommitReceivedInput!){
+6 -3
View File
@@ -1,6 +1,5 @@
from typing import Dict, List
from gql import gql
from graphql.language import parser
from specklepy.api.resource import ResourceBase
from specklepy.objects.base import Base
@@ -11,9 +10,13 @@ METHODS = []
class Resource(ResourceBase):
"""API Access class for objects"""
def __init__(self, me, basepath, client) -> None:
def __init__(self, account, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
self.schema = Base
+11 -3
View File
@@ -1,8 +1,8 @@
from typing import Dict, List
from gql import gql
from gql.client import Client
from specklepy.api.models import ServerInfo
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
NAME = "server"
@@ -12,9 +12,13 @@ METHODS = ["get", "apps"]
class Resource(ResourceBase):
"""API Access class for the server"""
def __init__(self, me, basepath, client) -> None:
def __init__(self, account, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
def get(self) -> ServerInfo:
@@ -23,6 +27,7 @@ class Resource(ResourceBase):
Returns:
dict -- the server info in dictionary form
"""
metrics.track(metrics.SERVER, self.account, {"name": "get"})
query = gql(
"""
query Server {
@@ -62,6 +67,7 @@ class Resource(ResourceBase):
Returns:
dict -- a dictionary of apps registered on the server
"""
metrics.track(metrics.SERVER, self.account, {"name": "apps"})
query = gql(
"""
query Apps {
@@ -95,6 +101,7 @@ class Resource(ResourceBase):
Returns:
str -- the new API token. note: this is the only time you'll see the token!
"""
metrics.track(metrics.SERVER, self.account, {"name": "create_token"})
query = gql(
"""
mutation TokenCreate($token: ApiTokenCreateInput!) {
@@ -120,6 +127,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if the token was successfully deleted
"""
metrics.track(metrics.SERVER, self.account, {"name": "revoke_token"})
query = gql(
"""
mutation TokenRevoke($token: String!) {
+178 -108
View File
@@ -1,27 +1,26 @@
from datetime import datetime, timezone
from gql import gql
from typing import Dict, List, Optional
from typing import List
from specklepy.logging import metrics
from specklepy.api.models import Stream
from specklepy.api.models import ActivityCollection, Stream
from specklepy.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "stream"
METHODS = [
"list",
"create",
"get",
"update",
"delete",
"search",
]
METHODS = ["list", "create", "get", "update", "delete", "search", "activity"]
class Resource(ResourceBase):
"""API Access class for streams"""
def __init__(self, me, basepath, client) -> None:
def __init__(self, account, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
self.schema = Stream
@@ -37,45 +36,45 @@ class Resource(ResourceBase):
Returns:
Stream -- the retrieved stream
"""
metrics.track(metrics.STREAM_GET)
metrics.track(metrics.STREAM, self.account, {"name": "get"})
query = gql(
"""
query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) {
stream(id: $id) {
id
name
description
isPublic
createdAt
updatedAt
collaborators {
id
name
role
avatar
}
branches(limit: $branch_limit) {
totalCount
cursor
items {
stream(id: $id) {
id
name
description
commits(limit: $commit_limit) {
totalCount
cursor
items {
isPublic
createdAt
updatedAt
collaborators {
id
referencedObject
message
authorName
authorId
createdAt
}
name
role
avatar
}
}
branches(limit: $branch_limit) {
totalCount
cursor
items {
id
name
description
commits(limit: $commit_limit) {
totalCount
cursor
items {
id
referencedObject
message
authorName
authorId
createdAt
}
}
}
}
}
}
}
"""
)
@@ -93,38 +92,38 @@ class Resource(ResourceBase):
Returns:
List[Stream] -- A list of Stream objects
"""
metrics.track(metrics.STREAM_LIST)
metrics.track(metrics.STREAM, self.account, {"name": "get"})
query = gql(
"""
query User($stream_limit: Int!) {
user {
id
email
name
bio
company
avatar
verified
profiles
role
streams(limit: $stream_limit) {
totalCount
cursor
items {
user {
id
email
name
description
isPublic
createdAt
updatedAt
collaborators {
id
name
role
}
bio
company
avatar
verified
profiles
role
streams(limit: $stream_limit) {
totalCount
cursor
items {
id
name
description
isPublic
createdAt
updatedAt
collaborators {
id
name
role
}
}
}
}
}
}
}
"""
)
@@ -151,7 +150,7 @@ class Resource(ResourceBase):
Returns:
id {str} -- the id of the newly created stream
"""
metrics.track(metrics.STREAM_CREATE)
metrics.track(metrics.STREAM, self.account, {"name": "create"})
query = gql(
"""
mutation StreamCreate($stream: StreamCreateInput!) {
@@ -182,11 +181,11 @@ class Resource(ResourceBase):
Returns:
bool -- whether the stream update was successful
"""
metrics.track(metrics.STREAM_UPDATE)
metrics.track(metrics.STREAM, self.account, {"name": "update"})
query = gql(
"""
mutation StreamUpdate($stream: StreamUpdateInput!) {
streamUpdate(stream: $stream)
streamUpdate(stream: $stream)
}
"""
)
@@ -213,13 +212,13 @@ class Resource(ResourceBase):
Returns:
bool -- whether the deletion was successful
"""
metrics.track(metrics.STREAM_DELETE)
metrics.track(metrics.STREAM, self.account, {"name": "delete"})
query = gql(
"""
mutation StreamDelete($id: String!) {
streamDelete(id: $id)
streamDelete(id: $id)
}
"""
"""
)
params = {"id": id}
@@ -246,47 +245,47 @@ class Resource(ResourceBase):
Returns:
List[Stream] -- a list of Streams that match the search query
"""
metrics.track(metrics.STREAM_SEARCH)
metrics.track(metrics.STREAM, self.account, {"name": "search"})
query = gql(
"""
query StreamSearch($search_query: String!,$limit: Int!, $branch_limit:Int!, $commit_limit:Int!) {
streams(query: $search_query, limit: $limit) {
items {
id
name
description
isPublic
createdAt
updatedAt
collaborators {
id
name
role
avatar
}
branches(limit: $branch_limit) {
totalCount
cursor
streams(query: $search_query, limit: $limit) {
items {
id
name
description
commits(limit: $commit_limit) {
totalCount
cursor
items {
id
referencedObject
message
authorName
authorId
createdAt
id
name
description
isPublic
createdAt
updatedAt
collaborators {
id
name
role
avatar
}
branches(limit: $branch_limit) {
totalCount
cursor
items {
id
name
description
commits(limit: $commit_limit) {
totalCount
cursor
items {
id
referencedObject
message
authorName
authorId
createdAt
}
}
}
}
}
}
}
}
}
}
"""
)
@@ -313,6 +312,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role})
query = gql(
"""
mutation StreamGrantPermission($permission_params: StreamGrantPermissionInput !) {
@@ -346,6 +346,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.PERMISSION, self.account, {"name": "revoke"})
query = gql(
"""
mutation StreamRevokePermission($permission_params: StreamRevokePermissionInput !) {
@@ -362,3 +363,72 @@ class Resource(ResourceBase):
return_type="streamRevokePermission",
parse_response=False,
)
def activity(
self,
stream_id: str,
action_type: str = None,
limit: int = 20,
before: datetime = None,
after: datetime = None,
cursor: datetime = None,
):
"""
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
stream_id {str} -- the id of the stream to get activity from
action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query StreamActivity($stream_id: String!, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
stream(id: $stream_id) {
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
try:
params = {
"stream_id": stream_id,
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat()
if before
else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat()
if cursor
else cursor,
}
except AttributeError as e:
raise SpeckleException(
"Could not get stream activity - `before`, `after`, and `cursor` must be in `datetime` format if provided",
ValueError,
) from e
return self.make_request(
query=query,
params=params,
return_type=["stream", "activity"],
schema=ActivityCollection,
)
+8 -4
View File
@@ -1,9 +1,9 @@
from typing import Callable, Dict, List, Optional, Any
from typing import Callable, Dict, List
from functools import wraps
from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.api.resources.stream import Stream
from specklepy.logging.exceptions import GraphQLException, SpeckleException
from specklepy.logging.exceptions import SpeckleException
NAME = "subscribe"
METHODS = [
@@ -29,9 +29,13 @@ def check_wsclient(function):
class Resource(ResourceBase):
"""API Access class for subscriptions"""
def __init__(self, me, basepath, client) -> None:
def __init__(self, account, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
@check_wsclient
+76 -6
View File
@@ -1,20 +1,25 @@
from datetime import datetime, timezone
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from typing import List, Optional
from typing import List
from gql import gql
from pydantic.main import BaseModel
from specklepy.api.resource import ResourceBase
from specklepy.api.models import User
from specklepy.api.models import ActivityCollection, User
NAME = "user"
METHODS = ["get"]
METHODS = ["get", "search", "update", "activity"]
class Resource(ResourceBase):
"""API Access class for users"""
def __init__(self, me, basepath, client) -> None:
def __init__(self, account, basepath, client) -> None:
super().__init__(
me=me, basepath=basepath, client=client, name=NAME, methods=METHODS
account=account,
basepath=basepath,
client=client,
name=NAME,
methods=METHODS,
)
self.schema = User
@@ -27,6 +32,7 @@ class Resource(ResourceBase):
Returns:
User -- the retrieved user
"""
metrics.track(metrics.USER, self.account, {"name": "get"})
query = gql(
"""
query User($id: String) {
@@ -63,6 +69,7 @@ class Resource(ResourceBase):
message="User search query must be at least 3 characters"
)
metrics.track(metrics.USER, self.account, {"name": "search"})
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
@@ -99,6 +106,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.USER, self.account, {"name": "update"})
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
@@ -118,3 +126,65 @@ class Resource(ResourceBase):
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
def activity(
self,
user_id: str = None,
limit: int = 20,
action_type: str = None,
before: datetime = None,
after: datetime = None,
cursor: datetime = None,
):
"""
Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity($user_id: String, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
user(id: $user_id) {
activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"user_id": user_id,
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["user", "activity"],
schema=ActivityCollection,
)
+1 -1
View File
@@ -445,7 +445,7 @@ input ServerInfoUpdateInput {
stream( id: String! ): Stream
"""
All the streams of the current user, pass in the `query` parameter to seach by name, description or ID.
All the streams of the current user, pass in the `query` parameter to search by name, description or ID.
"""
streams( query: String, limit: Int = 25, cursor: String ): StreamCollection
@hasScope(scope: "streams:read")
+166
View File
@@ -0,0 +1,166 @@
from warnings import warn
from urllib.parse import urlparse, unquote
from specklepy.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
)
from specklepy.logging import metrics
from specklepy.api.client import SpeckleClient
from specklepy.transports.server.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
class StreamWrapper:
"""
The `StreamWrapper` gives you some handy helpers to deal with urls and get authenticated clients and transports.
Construct a `StreamWrapper` with a stream, branch, commit, or object URL. The corresponding ids will be stored
in the wrapper. If you have local accounts on the machine, you can use the `get_account` and `get_client` methods
to get a local account for the server. You can also pass a token into `get_client` if you don't have a corresponding
local account for the server.
```py
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
# get an authenticated ServerTransport if you have a local account for the server
transport = wrapper.get_transport()
```
"""
stream_url: str = None
use_ssl: bool = True
host: str = None
stream_id: str = None
commit_id: str = None
object_id: str = None
branch_name: str = None
_client: SpeckleClient = None
_account: Account = None
def __repr__(self):
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
def __str__(self) -> str:
return self.__repr__()
@property
def type(self) -> str:
if self.object_id:
return "object"
elif self.commit_id:
return "commit"
elif self.branch_name:
return "branch"
else:
return "stream" if self.stream_id else "invalid"
def __init__(self, url: str) -> None:
self.stream_url = url
parsed = urlparse(url)
self.host = parsed.netloc
self.use_ssl = parsed.scheme == "https"
segments = parsed.path.strip("/").split("/", 3)
metrics.track(metrics.STREAM_WRAPPER, self.get_account())
if not segments or len(segments) < 2:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
while segments:
segment = segments.pop(0)
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
@property
def server_url(self):
return f"{'https' if self.use_ssl else 'http'}://{self.host}"
def get_account(self, token: str = None) -> Account:
"""
Gets an account object for this server from the local accounts db (added via Speckle Manager or a json file)
"""
if self._account and self._account.token:
return self._account
self._account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
None,
)
if not self._account:
self._account = get_account_from_token(token, self.server_url)
if self._client:
self._client.authenticate_with_account(self._account)
return self._account
def get_client(self, token: str = None) -> SpeckleClient:
"""
Gets an authenticated client for this server. You may provide a token if there aren't any local accounts on this
machine. If no account is found and no token is provided, an unauthenticated client is returned.
Arguments:
token {str} -- optional token if no local account is available (defaults to None)
Returns:
SpeckleClient -- authenticated with a corresponding local account or the provided token
"""
if self._client and token is None:
return self._client
if not self._account or not self._account.token:
self.get_account(token)
if not self._client:
self._client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if self._account.token is None and token is None:
warn(f"No local account found for server {self.host}", SpeckleWarning)
return self._client
if self._account.token:
self._client.authenticate_with_account(self._account)
else:
self._client.authenticate_with_token(token)
return self._client
def get_transport(self, token: str = None) -> ServerTransport:
"""
Gets a server transport for this stream using an authenticated client. If there is no local account for this
server and the client was not authenticated with a token, this will throw an exception.
Returns:
ServerTransport -- constructed for this stream with a pre-authenticated client
"""
if not self._account or not self._account.token:
self.get_account(token)
return ServerTransport(self.stream_id, account=self._account)
+71 -54
View File
@@ -1,10 +1,10 @@
import os
import socket
import sys
import queue
import hashlib
import logging
import requests
import threading
from requests.sessions import session
from specklepy.transports.sqlite import SQLiteTransport
"""
Anonymous telemetry to help us understand how to make a better Speckle.
@@ -12,25 +12,25 @@ This really helps us to deliver a better open source project and product!
"""
TRACK = True
HOST_APP = "python"
HOST_APP_VERSION = f"python {'.'.join(map(str, sys.version_info[:3]))}"
PLATFORMS = {"win32": "Windows", "cygwin": "Windows", "darwin": "Mac OS X"}
LOG = logging.getLogger(__name__)
METRICS_TRACKER = None
# actions
RECEIVE = "receive"
SEND = "send"
STREAM_CREATE = "stream/create"
STREAM_GET = "stream/get"
STREAM_UPDATE = "stream/update"
STREAM_DELETE = "stream/delete"
STREAM_DETAILS = "stream/details"
STREAM_LIST = "stream/list"
STREAM_VIEW = "stream/view"
STREAM_SEARCH = "stream/search"
RECEIVE = "Receive"
SEND = "Send"
STREAM = "Stream Action"
PERMISSION = "Permission Action"
COMMIT = "Commit Action"
BRANCH = "Branch Action"
USER = "User Action"
SERVER = "Server Action"
CLIENT = "Speckle Client"
STREAM_WRAPPER = "Stream Wrapper"
ACCOUNT_DEFAULT = "account/default"
ACCOUNT_DETAILS = "account/details"
ACCOUNT_LIST = "account/list"
ACCOUNTS = "Get Local Accounts"
SERIALIZE = "serialization/serialize"
DESERIALIZE = "serialization/deserialize"
@@ -41,44 +41,54 @@ def disable():
TRACK = False
def set_host_app(host_app: str):
def enable():
global TRACK
TRACK = True
def set_host_app(host_app: str, host_app_version: str = None):
global HOST_APP
HOST_APP = host_app
HOST_APP_VERSION = host_app_version or HOST_APP_VERSION
def track(action: str):
def track(action: str, account: "Account" = None, custom_props: dict = None):
if not TRACK:
return
try:
global METRICS_TRACKER
if not METRICS_TRACKER:
METRICS_TRACKER = MetricsTracker()
page_params = {
"rec": 1,
"idsite": METRICS_TRACKER.site_id,
"uid": METRICS_TRACKER.suuid,
"action_name": action,
"url": f"http://connectors/{HOST_APP}/{action}",
"urlref": f"http://connectors/{HOST_APP}/{action}",
"_cvar": {"1": ["hostApplication", HOST_APP]},
}
initialise_tracker(account)
event_params = {
"rec": 1,
"idsite": METRICS_TRACKER.site_id,
"uid": MetricsTracker.suuid,
"_cvar": {"1": ["hostApplication", HOST_APP]},
"e_c": HOST_APP,
"e_a": action,
"event": action,
"properties": {
"distinct_id": METRICS_TRACKER.last_user,
"server_id": METRICS_TRACKER.last_server,
"token": METRICS_TRACKER.analytics_token,
"hostApp": HOST_APP,
"hostAppVersion": HOST_APP_VERSION,
"$os": METRICS_TRACKER.platform,
"type": "action",
},
}
if custom_props:
event_params["properties"].update(custom_props)
METRICS_TRACKER.queue.put_nowait([event_params, page_params])
METRICS_TRACKER.queue.put_nowait(event_params)
except Exception as ex:
# wrapping this whole thing in a try except as we never want a failure here to annoy users!
LOG.error("Error queueing metrics request: " + str(ex))
def initialise_tracker(account: "Account" = None):
global METRICS_TRACKER
if not METRICS_TRACKER:
METRICS_TRACKER = MetricsTracker()
if account and account.userInfo.email:
METRICS_TRACKER.set_last_user(account.userInfo.email)
if account and account.serverInfo.url:
METRICS_TRACKER.set_last_server(account.userInfo.email)
class Singleton(type):
_instances = {}
@@ -89,10 +99,12 @@ class Singleton(type):
class MetricsTracker(metaclass=Singleton):
matomo_url = "https://speckle.matomo.cloud/matomo.php"
site_id = 2
host_app = "python"
suuid = None
analytics_url = "https://analytics.speckle.systems/track?ip=1"
analytics_token = "acd87c5a50b56df91a795e999812a3a4"
user_ip = None
last_user = None
last_server = None
platform = None
sending_thread = None
queue = queue.Queue(1000)
@@ -100,25 +112,30 @@ class MetricsTracker(metaclass=Singleton):
self.sending_thread = threading.Thread(
target=self._send_tracking_requests, daemon=True
)
self.set_suuid()
self.platform = PLATFORMS.get(sys.platform, "linux")
self.sending_thread.start()
self.user_ip = socket.gethostbyname(socket.gethostname())
def set_suuid(self):
try:
file_path = os.path.join(SQLiteTransport.get_base_path("Speckle"), "suuid")
with open(file_path, "r") as file:
self.suuid = file.read()
except:
self.suuid = "unknown-suuid"
def set_last_user(self, email: str):
if not email:
return
self.last_user = "@" + self.hash(email)
def set_last_server(self, server: str):
if not server:
return
self.last_server = self.hash(server)
def hash(self, value: str):
return hashlib.md5(value.lower().encode("utf-8")).hexdigest().upper()
def _send_tracking_requests(self):
session = requests.Session()
while True:
params = self.queue.get()
event_params = [self.queue.get()]
try:
session.post(self.matomo_url, params=params[0])
session.post(self.matomo_url, params=params[1])
session.post(self.analytics_url, json=event_params)
except Exception as ex:
LOG.error("Error sending metrics request: " + str(ex))
+11 -7
View File
@@ -1,15 +1,17 @@
import typing
from typing import (
Any,
Callable,
ClassVar,
Dict,
List,
Optional,
Union,
Set,
Type,
get_type_hints,
)
import contextlib
from enum import EnumMeta
from warnings import warn
from specklepy.logging.exceptions import SpeckleException
@@ -249,17 +251,20 @@ class Base(_RegisteringBase):
types = getattr(self, "_attr_types", {})
t = types.get(name, None)
if t is None:
if t is None or t is Any:
return value
if value is None:
return None
if isinstance(t, EnumMeta) and (value in t._value2member_map_):
return t(value)
if t.__module__ == "typing":
origin = getattr(t, "__origin__")
t = (
tuple(getattr(sub_t, "__origin__", sub_t) for sub_t in t.__args__)
if origin is typing.Union
if origin is Union
else origin
)
@@ -276,13 +281,11 @@ class Base(_RegisteringBase):
if isinstance(t, tuple):
t = t[0]
try:
with contextlib.suppress(ValueError):
if t is float:
return float(value)
if t is str and value:
return str(value)
except ValueError:
pass
raise SpeckleException(
f"Cannot set '{self.__class__.__name__}.{name}': it expects type '{t.__name__}', but received type '{type(value).__name__}'"
@@ -406,4 +409,5 @@ class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: List[Any] = None
def __init__(self) -> None:
super().__init__()
self.data = []
+1 -1
View File
@@ -74,7 +74,7 @@ class ObjectArray:
index = 0
while index < len(data):
item_length = data[index]
item_length = int(data[index])
item_start = index + 1
item_end = item_start + item_length
item_data = data[item_start:item_end]
+10 -1
View File
@@ -1,5 +1,6 @@
from specklepy.objects.geometry import Point
from enum import Enum
from typing import List
from specklepy.objects.geometry import Point
from .base import Base
@@ -19,11 +20,19 @@ class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
dots: List[int] = None
class FakeDirection(Enum):
NORTH = 1
EAST = 2
SOUTH = 3
WEST = 4
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
vertices: List[float] = None
faces: List[int] = None
colors: List[int] = None
textureCoordinates: List[float] = None
cardinal_dir: FakeDirection = None
test_bases: List[Base] = None
detach_this: Base = None
detached_list: List[Base] = None
+17 -5
View File
@@ -293,9 +293,9 @@ class Curve(
@classmethod
def from_list(cls, args: List[Any]) -> "Curve":
point_count = args[7]
weights_count = args[8]
knots_count = args[9]
point_count = int(args[7])
weights_count = int(args[8])
knots_count = int(args[9])
points_start = 10
weights_start = 10 + point_count
@@ -303,7 +303,7 @@ class Curve(
knots_end = knots_start + knots_count
return cls(
degree=args[1],
degree=int(args[1]),
periodic=bool(args[2]),
rational=bool(args[3]),
closed=bool(args[4]),
@@ -632,7 +632,7 @@ class Brep(
bbox: Box = None
area: float = None
volume: float = None
displayValue: Mesh = None
_displayValue: List[Mesh] = None
Surfaces: List[Surface] = None
Curve3D: List[Base] = None
Curve2D: List[Base] = None
@@ -648,6 +648,18 @@ class Brep(
child._Brep = self
return children
# set as prop for now for backwards compatibility
@property
def displayValue(self) -> List[Mesh]:
return self._displayValue
@displayValue.setter
def displayValue(self, value):
if isinstance(value, Mesh):
self._displayValue = [value]
elif isinstance(value, list):
self._displayValue = value
@property
def Edges(self) -> List[BrepEdge]:
return self._inject_self_into_children(self._Edges)
+19 -4
View File
@@ -1,4 +1,4 @@
from typing import List
from typing import Any, List
from specklepy.objects.geometry import Point, Vector
from .base import Base
@@ -55,10 +55,11 @@ class Transform(
def value(self, value: List[float]) -> None:
try:
value = [float(x) for x in value]
except (ValueError, TypeError):
except (ValueError, TypeError) as error:
raise ValueError(
f"Could not create a Transform object with the requested value. Input must be a 16 element list of numbers. Value provided: {value}"
)
) from error
if len(value) != 16:
raise ValueError(
f"Could not create a Transform object: input list should be 16 floats long, but was {len(value)} long"
@@ -196,4 +197,18 @@ class BlockInstance(
Base, speckle_type=OTHER + "BlockInstance", detachable={"blockDefinition"}
):
blockDefinition: BlockDefinition = None
transform: Transform = None
transform: Transform = None
# TODO: prob move this into a built elements module, but just trialling this for now
class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter"):
name: str = None
value: Any = None
applicationUnitType: str = None # eg UnitType UT_Length
applicationUnit: str = None # DisplayUnitType eg DUT_MILLIMITERS
applicationInternalName: str = (
None # BuiltInParameterName or GUID for shared parameter
)
isShared: bool = False
isReadOnly: bool = False
isTypeParameter: bool = False
+1 -3
View File
@@ -1,6 +1,4 @@
from enum import Enum
import enum
from typing import Any, List, Optional
from typing import List
from ..base import Base
from ..geometry import *
+1 -2
View File
@@ -1,6 +1,5 @@
from enum import Enum
import enum
from typing import Any, List, Optional
from typing import List
from ..base import Base
from ..geometry import *
+1 -1
View File
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Any, List, Optional
from typing import List
from ..base import Base
from .geometry import *
-1
View File
@@ -1,5 +1,4 @@
from enum import Enum
from typing import Any, List, Optional
from ..base import Base
@@ -1,6 +1,4 @@
from enum import Enum
from typing import Any, List, Optional
from ..base import Base
+1 -3
View File
@@ -1,6 +1,4 @@
from enum import Enum
import enum
from typing import Any, List, Optional
from typing import List
from ..base import Base
from ..geometry import *
@@ -2,6 +2,7 @@ import ujson
import hashlib
import re
from uuid import uuid4
from enum import Enum
from warnings import warn
from typing import Any, Dict, List, Tuple
from specklepy.objects.base import Base, DataChunk
@@ -111,6 +112,11 @@ class BaseObjectSerializer:
object_builder[prop] = value
continue
# NOTE: for dynamic props, this won't be re-serialised as an enum but as an int
if isinstance(value, Enum):
object_builder[prop] = value.value
continue
# 2. handle Base objects
elif isinstance(value, Base):
child_obj = self.traverse_value(value, detach=detach)
@@ -182,6 +188,10 @@ class BaseObjectSerializer:
if isinstance(obj, PRIMITIVES):
return obj
# NOTE: for dynamic props, this won't be re-serialised as an enum but as an int
if isinstance(obj, Enum):
return obj.value
elif isinstance(obj, (list, tuple, set)):
if not detach:
return [self.traverse_value(o) for o in obj]
@@ -198,7 +208,7 @@ class BaseObjectSerializer:
elif isinstance(obj, dict):
for k, v in obj.items():
if isinstance(v, PRIMITIVES):
if isinstance(v, PRIMITIVES) or v is None:
continue
else:
obj[k] = self.traverse_value(v)
@@ -213,10 +223,11 @@ class BaseObjectSerializer:
try:
return obj.dict()
except:
SerializationException(
message=f"Failed to handle {type(obj)} in `BaseObjectSerializer.traverse_value`",
object=obj,
warn(
f"Failed to handle {type(obj)} in `BaseObjectSerializer.traverse_value`",
SpeckleWarning,
)
return str(obj)
def detach_helper(self, ref_hash: str) -> Dict[str, str]:
+1 -1
View File
@@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import Any, Optional, List, Dict
from typing import Optional, List, Dict
from pydantic import BaseModel
from pydantic.main import Extra
+1 -6
View File
@@ -1,6 +1,4 @@
import json
from typing import Any, List, Dict
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
@@ -28,10 +26,7 @@ class MemoryTransport(AbstractTransport):
raise NotImplementedError
def get_object(self, id: str) -> str or None:
if id in self.objects:
return self.objects[id]
else:
return None
return self.objects[id] if id in self.objects else None
def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
return {id: (id in self.objects) for id in id_list}
@@ -2,7 +2,6 @@ import json
import logging
import threading
import queue
import time
import gzip
import requests
+17 -10
View File
@@ -1,11 +1,11 @@
import json
import time
import requests
from warnings import warn
from typing import Any, Dict, List, Type
from typing import Any, Dict, List
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.abstract_transport import AbstractTransport
@@ -19,7 +19,8 @@ class ServerTransport(AbstractTransport):
The `ServerTransport` can be authenticted two different ways:
1. by providing a `SpeckleClient`
2. by providing a `token` and `url`
2. by providing an `Account`
3. by providing a `token` and `url`
```py
from specklepy.api import operations
@@ -46,6 +47,7 @@ class ServerTransport(AbstractTransport):
_name = "RemoteTransport"
url: str = None
stream_id: str = None
account: Account = None
saved_obj_count: int = 0
session: requests.Session = None
@@ -53,38 +55,43 @@ class ServerTransport(AbstractTransport):
self,
stream_id: str,
client: SpeckleClient = None,
account: Account = None,
token: str = None,
url: str = None,
**data: Any,
) -> None:
super().__init__(**data)
# TODO: replace client with account or some other auth avenue
if client is None and token is None and url is None:
if client is None and account is None and token is None and url is None:
raise SpeckleException(
"You must provide either a client or a token and url to construct a ServerTransport."
)
if client:
if account:
self.account = account
url = account.serverInfo.url
elif client:
url = client.url
if not client.me:
if not client.account.token:
warn(
SpeckleWarning(
f"Unauthenticated Speckle Client provided to Server Transport for {self.url}. Receiving from private streams will fail."
)
)
else:
token = client.me["token"]
self.account = client.account
else:
self.account = get_account_from_token(token, url)
self.stream_id = stream_id
self.url = url
self._batch_sender = BatchSender(
self.url, self.stream_id, token, max_batch_size_mb=1
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
self.session = requests.Session()
self.session.headers.update(
{"Authorization": f"Bearer {token}", "Accept": "text/plain"}
{"Authorization": f"Bearer {self.account.token}", "Accept": "text/plain"}
)
def begin_write(self) -> None:
+7 -2
View File
@@ -6,7 +6,7 @@ from specklepy.api.models import Stream
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.objects.geometry import Point
from specklepy.objects.fakemesh import FakeMesh
from specklepy.objects.fakemesh import FakeDirection, FakeMesh
from specklepy.logging import metrics
metrics.disable()
@@ -81,9 +81,10 @@ def mesh():
mesh = FakeMesh()
mesh.name = "my_mesh"
mesh.vertices = [random.uniform(0, 10) for _ in range(1, 210)]
mesh.faces = [i for i in range(1, 210)]
mesh.faces = list(range(1, 210))
mesh["@(100)colours"] = [random.uniform(0, 10) for _ in range(1, 210)]
mesh["@()default_chunk"] = [random.uniform(0, 10) for _ in range(1, 210)]
mesh.cardinal_dir = FakeDirection.WEST
mesh.test_bases = [Base(name=f"test {i}") for i in range(1, 22)]
mesh.detach_this = Base(name="predefined detached base")
mesh["@detach"] = Base(name="detached base")
@@ -102,6 +103,10 @@ def base():
base = Base()
base.name = "my_base"
base.units = "millimetres"
base.null_val = None
base.null_dict = {"a null val": None}
base.tuple = (1, 2, "3")
base.set = {1, 2, "3"}
base.vertices = [random.uniform(0, 10) for _ in range(1, 120)]
base.test_bases = [Base(name=i) for i in range(1, 22)]
base["@detach"] = Base(name="detached base")
+12 -1
View File
@@ -1,5 +1,6 @@
from contextlib import ExitStack as does_not_raise
from enum import Enum
from typing import Dict, List, Optional
from contextlib import ExitStack as does_not_raise
import pytest
from specklepy.api import operations
@@ -95,6 +96,12 @@ def test_base_of_custom_speckle_type() -> None:
assert b1.name == "Tweety's Crib"
class DietaryRestrictions(Enum):
VEGAN = 1
GLUTEN_FREE = 2
NUT_FREE = 3
class FrozenYoghurt(Base):
"""Testing type checking"""
@@ -103,6 +110,7 @@ class FrozenYoghurt(Base):
customer: str
add_ons: Optional[Dict[str, float]] # dict item types won't be checked
price: float = 0.0
dietary: DietaryRestrictions
def test_type_checking() -> None:
@@ -111,6 +119,7 @@ def test_type_checking() -> None:
order.servings = 2
order.price = "7" # will get converted
order.customer = "izzy"
order.dietary = DietaryRestrictions.VEGAN
with pytest.raises(SpeckleException):
order.flavours = "not a list"
@@ -118,6 +127,8 @@ def test_type_checking() -> None:
order.servings = "five"
with pytest.raises(SpeckleException):
order.add_ons = ["sprinkles"]
with pytest.raises(SpeckleException):
order.dietary = "no nuts plz"
order.add_ons = {"sprinkles": 0.2, "chocolate": 1.0}
order.flavours = ["strawberry", "lychee", "peach", "pineapple"]
+20 -3
View File
@@ -3,6 +3,7 @@ from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.objects.base import Base
from specklepy.transports.server import ServerTransport
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
@@ -10,12 +11,12 @@ def test_invalid_authentication():
client = SpeckleClient()
with pytest.warns(SpeckleWarning):
client.authenticate("fake token")
client.authenticate_with_token("fake token")
def test_invalid_send():
client = SpeckleClient()
client.me = {"token": "fake token"}
client.account = Account(token="fake_token")
transport = ServerTransport("3073b96e86", client)
with pytest.raises(SpeckleException):
@@ -24,8 +25,24 @@ def test_invalid_send():
def test_invalid_receive():
client = SpeckleClient()
client.me = {"token": "fake token"}
client.account = Account(token="fake_token")
transport = ServerTransport("fake stream", client)
with pytest.raises(SpeckleException):
operations.receive("fake object", transport)
def test_account_from_token():
token = "fake token"
acct = get_account_from_token(token)
assert acct.token == token
def test_account_from_token_and_url():
token = "fake token"
url = "fake.server"
acct = get_account_from_token(token, url)
assert acct.token == token
assert acct.serverInfo.url == url
+1 -1
View File
@@ -55,7 +55,7 @@ class TestSerialization:
# also try constructing server transport with token and url
transport = ServerTransport(
stream_id=sample_stream.id, token=client.me["token"], url=client.url
stream_id=sample_stream.id, token=client.account.token, url=client.url
)
# use a fresh memory transport to force receiving from remote
received = operations.receive(
+17 -2
View File
@@ -1,5 +1,7 @@
import pytest
from specklepy.api.models import Stream
from datetime import datetime
from specklepy.api.models import ActivityCollection, Activity, Stream
from specklepy.api.client import SpeckleClient
from specklepy.logging.exceptions import GraphQLException
@@ -87,9 +89,22 @@ class TestStream:
fetched_stream = client.stream.get(stream.id)
assert revoked == True
assert revoked is True
assert len(fetched_stream.collaborators) == 1
def test_stream_activity(self, client: SpeckleClient, stream: Stream):
activity = client.stream.activity(stream.id)
older_activity = client.stream.activity(
stream.id, before=activity.items[0].time
)
assert isinstance(activity, ActivityCollection)
assert isinstance(older_activity, ActivityCollection)
assert older_activity.totalCount < activity.totalCount
assert activity.items is not None
assert isinstance(activity.items[0], Activity)
def test_stream_delete(self, client, stream):
deleted = client.stream.delete(stream.id)
+17 -2
View File
@@ -1,6 +1,7 @@
from specklepy.logging.exceptions import SpeckleException
from specklepy.api.models import User
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.api.models import Activity, ActivityCollection, User
from specklepy.logging.exceptions import SpeckleException
@pytest.mark.run(order=1)
@@ -43,3 +44,17 @@ class TestUser:
assert isinstance(failed_update, SpeckleException)
assert updated is True
assert me.bio == bio
def test_user_activity(self, client: SpeckleClient, second_user_dict):
my_activity = client.user.activity(limit=10)
their_activity = client.user.activity(second_user_dict["id"])
assert isinstance(my_activity, ActivityCollection)
assert isinstance(my_activity.items[0], Activity)
assert my_activity.totalCount > 0
assert isinstance(their_activity, ActivityCollection)
older_activity = client.user.activity(before=my_activity.items[0].time)
assert isinstance(older_activity, ActivityCollection)
assert older_activity.totalCount < my_activity.totalCount
+64 -62
View File
@@ -1,79 +1,81 @@
import pytest
from specklepy.api.credentials import StreamWrapper
from specklepy.api.wrapper import StreamWrapper
class TestWrapper:
def test_parse_stream(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
assert wrap.type == "stream"
def test_parse_stream():
wrap = StreamWrapper("https://testing.speckle.dev/streams/a75ab4f10f")
assert wrap.type == "stream"
def test_parse_branch(self):
wacky_wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F"
)
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/next%20level"
)
assert wacky_wrap.type == "branch"
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
assert wrap.type == "branch"
def test_parse_nested_branch(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/izzy/dev"
)
def test_parse_branch():
wacky_wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/%F0%9F%8D%95%E2%AC%85%F0%9F%8C%9F%20you%20wat%3F"
)
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/next%20level"
)
assert wacky_wrap.type == "branch"
assert wacky_wrap.branch_name == "🍕⬅🌟 you wat?"
assert wrap.type == "branch"
assert wrap.branch_name == "izzy/dev"
assert wrap.type == "branch"
def test_parse_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
)
assert wrap.type == "commit"
def test_parse_nested_branch():
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/branches/izzy/dev"
)
def test_parse_object(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6"
)
assert wrap.type == "object"
assert wrap.branch_name == "izzy/dev"
assert wrap.type == "branch"
def test_parse_globals_as_branch(self):
wrap = StreamWrapper("https://testing.speckle.dev/streams/0c6ad366c4/globals/")
assert wrap.type == "branch"
def test_parse_globals_as_commit(self):
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893"
)
assert wrap.type == "commit"
def test_parse_commit():
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/4c3ce1459c/commits/8b9b831792"
)
assert wrap.type == "commit"
#! NOTE: the following three tests may not pass locally if you have a `speckle.xyz` account in manager
def test_get_client_without_auth(self):
wrap = StreamWrapper(
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
assert client is not None
def test_parse_object():
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/a75ab4f10f/objects/5530363e6d51c904903dafc3ea1d2ec6"
)
assert wrap.type == "object"
def test_get_new_client_with_token(self):
wrap = StreamWrapper(
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
client = wrap.get_client(token="super-secret-token")
assert client.me["token"] == "super-secret-token"
def test_parse_globals_as_branch():
wrap = StreamWrapper("https://testing.speckle.dev/streams/0c6ad366c4/globals/")
assert wrap.type == "branch"
def test_get_transport_with_token(self):
wrap = StreamWrapper(
"https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792"
)
client = wrap.get_client()
assert not client.me # unauthenticated bc no local accounts
transport = wrap.get_transport(token="super-secret-token")
def test_parse_globals_as_commit():
wrap = StreamWrapper(
"https://testing.speckle.dev/streams/0c6ad366c4/globals/abd3787893"
)
assert wrap.type == "commit"
assert transport is not None
assert client.me["token"] == "super-secret-token"
#! NOTE: the following three tests may not pass locally if you have a `speckle.xyz` account in manager
def test_get_client_without_auth():
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
client = wrap.get_client()
assert client is not None
def test_get_new_client_with_token():
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
client = wrap.get_client()
client = wrap.get_client(token="super-secret-token")
assert client.account.token == "super-secret-token"
def test_get_transport_with_token():
wrap = StreamWrapper("https://speckle.xyz/streams/4c3ce1459c/commits/8b9b831792")
client = wrap.get_client()
assert not client.account.token # unauthenticated bc no local accounts
transport = wrap.get_transport(token="super-secret-token")
assert transport is not None
assert client.account.token == "super-secret-token"