From 944e70221e07decb9314c72282ccab4ded7f768f Mon Sep 17 00:00:00 2001 From: izzy lyseggen Date: Wed, 23 Feb 2022 11:00:04 +0000 Subject: [PATCH] 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 --- poetry.lock | 229 +++++++++++++++++++++-- pyproject.toml | 1 + specklepy/api/client.py | 68 +++++-- specklepy/api/credentials.py | 197 +++++-------------- specklepy/api/operations.py | 9 +- specklepy/api/resource.py | 5 +- specklepy/api/resources/branch.py | 17 +- specklepy/api/resources/commit.py | 14 +- specklepy/api/resources/object.py | 8 +- specklepy/api/resources/server.py | 8 +- specklepy/api/resources/stream.py | 22 ++- specklepy/api/resources/subscriptions.py | 8 +- specklepy/api/resources/user.py | 14 +- specklepy/api/wrapper.py | 162 ++++++++++++++++ specklepy/logging/metrics.py | 117 +++++++----- specklepy/transports/memory.py | 5 +- specklepy/transports/server/server.py | 24 ++- tests/test_client_and_ops.py | 23 ++- tests/test_serialization.py | 2 +- tests/test_wrapper.py | 126 +++++++------ 20 files changed, 720 insertions(+), 339 deletions(-) create mode 100644 specklepy/api/wrapper.py diff --git a/poetry.lock b/poetry.lock index 1d87258..3deabd8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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"}, diff --git a/pyproject.toml b/pyproject.toml index 70d93d0..0b1ef4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/specklepy/api/client.py b/specklepy/api/client.py index 18e1517..ebcdd0c 100644 --- a/specklepy/api/client.py +++ b/specklepy/api/client.py @@ -1,5 +1,7 @@ import re from warnings import warn +from deprecated import deprecated +from specklepy.api.credentials import Account, get_account_from_token from specklepy.logging.exceptions import ( GraphQLException, SpeckleException, @@ -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") @@ -66,9 +68,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) @@ -88,10 +90,12 @@ class SpeckleClient: raise SpeckleException(f"{self.url} is not a compatible Speckle Server", ex) def __repr__(self): - return ( - f"SpeckleClient( server: {self.url}, authenticated: {self.me is not None} )" - ) + 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 +103,31 @@ 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) + 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` + """ + self.account = account + + def _set_up_client(self) -> None: headers = { - "Authorization": f"Bearer {self.me['token']}", + "Authorization": f"Bearer {self.account.token}", "Content-Type": "application/json", } httptransport = RequestsHTTPTransport( @@ -109,7 +135,7 @@ 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) @@ -119,7 +145,7 @@ class SpeckleClient: if isinstance(self.user.get(), GraphQLException): 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 +154,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 +180,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" diff --git a/specklepy/api/credentials.py b/specklepy/api/credentials.py index efcb1b6..7d10651 100644 --- a/specklepy/api/credentials.py +++ b/specklepy/api/credentials.py @@ -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, + ) diff --git a/specklepy/api/operations.py b/specklepy/api/operations.py index 6a2c6b7..df3e068 100644 --- a/specklepy/api/operations.py +++ b/specklepy/api/operations.py @@ -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 @@ -22,14 +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()) @@ -61,7 +66,7 @@ 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() diff --git a/specklepy/api/resource.py b/specklepy/api/resource.py index 8290fd6..f8931d2 100644 --- a/specklepy/api/resource.py +++ b/specklepy/api/resource.py @@ -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 diff --git a/specklepy/api/resources/branch.py b/specklepy/api/resources/branch.py index eb1ed70..f714342 100644 --- a/specklepy/api/resources/branch.py +++ b/specklepy/api/resources/branch.py @@ -1,6 +1,7 @@ from gql import gql from specklepy.api.resource import ResourceBase from specklepy.api.models import Branch +from specklepy.logging import metrics NAME = "branch" METHODS = ["create"] @@ -9,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 @@ -27,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!) { @@ -58,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!) { @@ -106,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!) { @@ -160,6 +166,7 @@ class Resource(ResourceBase): Returns: bool -- True if update is successfull """ + metrics.track(metrics.BRANCH, self.account, {"name": "update"}) query = gql( """ mutation BranchUpdate($branch: BranchUpdateInput!) { @@ -193,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!) { diff --git a/specklepy/api/resources/commit.py b/specklepy/api/resources/commit.py index 25d1cd5..38c1297 100644 --- a/specklepy/api/resources/commit.py +++ b/specklepy/api/resources/commit.py @@ -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!){ diff --git a/specklepy/api/resources/object.py b/specklepy/api/resources/object.py index a34c15f..7239b13 100644 --- a/specklepy/api/resources/object.py +++ b/specklepy/api/resources/object.py @@ -10,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 diff --git a/specklepy/api/resources/server.py b/specklepy/api/resources/server.py index 63ac773..f5e8f72 100644 --- a/specklepy/api/resources/server.py +++ b/specklepy/api/resources/server.py @@ -11,9 +11,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: diff --git a/specklepy/api/resources/stream.py b/specklepy/api/resources/stream.py index 296c9ba..b7893a1 100644 --- a/specklepy/api/resources/stream.py +++ b/specklepy/api/resources/stream.py @@ -19,9 +19,13 @@ METHODS = [ 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,7 +41,7 @@ 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!) { @@ -93,7 +97,7 @@ 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!) { @@ -151,7 +155,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,7 +186,7 @@ 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!) { @@ -213,7 +217,7 @@ 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!) { @@ -246,7 +250,7 @@ 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!) { @@ -313,6 +317,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 +351,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 !) { diff --git a/specklepy/api/resources/subscriptions.py b/specklepy/api/resources/subscriptions.py index bb61f72..a3fecd5 100644 --- a/specklepy/api/resources/subscriptions.py +++ b/specklepy/api/resources/subscriptions.py @@ -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 diff --git a/specklepy/api/resources/user.py b/specklepy/api/resources/user.py index c491310..0f5557b 100644 --- a/specklepy/api/resources/user.py +++ b/specklepy/api/resources/user.py @@ -1,3 +1,4 @@ +from specklepy.logging import metrics from specklepy.logging.exceptions import SpeckleException from typing import List from gql import gql @@ -5,15 +6,19 @@ from specklepy.api.resource import ResourceBase from specklepy.api.models import User NAME = "user" -METHODS = ["get"] +METHODS = ["get", "search", "update"] 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 @@ -26,6 +31,7 @@ class Resource(ResourceBase): Returns: User -- the retrieved user """ + metrics.track(metrics.USER, self.account, {"name": "get"}) query = gql( """ query User($id: String) { @@ -62,6 +68,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!) { @@ -98,6 +105,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!) { diff --git a/specklepy/api/wrapper.py b/specklepy/api/wrapper.py new file mode 100644 index 0000000..0e75469 --- /dev/null +++ b/specklepy/api/wrapper.py @@ -0,0 +1,162 @@ +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.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: + 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." + ) + + 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.host) + + 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) diff --git a/specklepy/logging/metrics.py b/specklepy/logging/metrics.py index d748dc8..99be0be 100644 --- a/specklepy/logging/metrics.py +++ b/specklepy/logging/metrics.py @@ -1,9 +1,12 @@ +import json import os +import socket +import sys import queue +import hashlib import logging import requests import threading -from specklepy.transports.sqlite import SQLiteTransport """ Anonymous telemetry to help us understand how to make a better Speckle. @@ -11,25 +14,22 @@ This really helps us to deliver a better open source project and product! """ TRACK = True HOST_APP = "python" +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" +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" @@ -40,44 +40,52 @@ def disable(): TRACK = False +def enable(): + global TRACK + TRACK = True + + def set_host_app(host_app: str): global HOST_APP HOST_APP = host_app -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, + "$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 = {} @@ -88,10 +96,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) @@ -99,25 +109,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)) diff --git a/specklepy/transports/memory.py b/specklepy/transports/memory.py index a7ddd5f..0f61bff 100644 --- a/specklepy/transports/memory.py +++ b/specklepy/transports/memory.py @@ -26,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} diff --git a/specklepy/transports/server/server.py b/specklepy/transports/server/server.py index a77df99..666e5da 100644 --- a/specklepy/transports/server/server.py +++ b/specklepy/transports/server/server.py @@ -5,6 +5,7 @@ from warnings import warn 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 @@ -18,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 @@ -45,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 @@ -52,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: diff --git a/tests/test_client_and_ops.py b/tests/test_client_and_ops.py index d4f9e45..1f85c46 100644 --- a/tests/test_client_and_ops.py +++ b/tests/test_client_and_ops.py @@ -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 \ No newline at end of file diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 0300431..3a47bc6 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -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( diff --git a/tests/test_wrapper.py b/tests/test_wrapper.py index 54818b6..a3840d5 100644 --- a/tests/test_wrapper.py +++ b/tests/test_wrapper.py @@ -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"