Compare commits

..

59 Commits

Author SHA1 Message Date
KatKatKateryna 284d841a1e Merge pull request #285 from specklesystems/gergo/lockResourceExecution
fix(resource): make sure no threading contention with the client can happen
2023-06-30 20:45:54 +08:00
Gergő Jedlicska 668fc5131f fix(resource): make sure no threading contention with the client can happen 2023-06-30 14:42:23 +02:00
Jedd Morgan 64926bd41d Merge pull request #279 from specklesystems/kate/separate_metrics
Kate/separate metrics
2023-06-27 15:53:43 +01:00
Jedd Morgan 1c9b186ea5 Merge pull request #275 from ciga2011/main
Fix Pointcloud bugs in geometry.py
2023-06-26 15:54:19 +01:00
KatKatKateryna ed9f1ad818 rotation 2023-06-21 17:21:50 +01:00
KatKatKateryna b83c30a1c9 added offsets to crs class 2023-06-21 12:45:18 +01:00
Gergő Jedlicska c079342a55 Merge pull request #273 from mortenengen/limit-batch-length
Fix: limit number of objects per batch sent to server
2023-06-14 14:03:16 +02:00
Gergő Jedlicska 6aa29d9b30 Merge branch 'main' into limit-batch-length 2023-06-14 13:58:57 +02:00
KatKatKateryna f456e4ddbb rename 2023-06-13 17:53:03 +01:00
KatKatKateryna dcd13224d0 added native_units for Degrees 2023-06-13 17:48:03 +01:00
KatKatKateryna 06952a5991 more specific domain identification 2023-06-13 12:17:27 +01:00
KatKatKateryna 6049049813 fixed server_id 2023-06-12 20:05:47 +01:00
Alan Rynne 75b7d30bd6 Merge branch 'main' into kate/separate_metrics 2023-06-12 15:35:34 +02:00
Alan Rynne 19e26318fc fix(ci): Integration tests now use new docker-compose file (#283)
* fix(ci): Initial test with new docker-compose file

* fix(ci): Don't use python orb

* fix(ci): Use python 3

* fix(ci): Use python3 everywhere

* fix(ci): whitespace PEBKAC

* fix(ci): use included pyenv installation

* fix(ci): Skip pyenv version if its the current one

* fix(ci): Reapply matrix build for integration testing

* fix(ci): Run exec $SHELL

* fix(ci): Removed exec step

* fix(ci): Remove test-old job
2023-06-09 11:46:03 +02:00
Matteo Cominetti 798dc7ff6a Merge pull request #282 from specklesystems/alan/ci/remove-github-actions
chore(ci): Remove old github actions from specklepy repo
2023-06-08 13:59:14 +01:00
Alan Rynne 0e4cff5904 chore(ci): Remove old github actions from specklepy repo 2023-06-08 10:53:02 +02:00
KatKatKateryna d1502c9072 add GIS classes 2023-06-02 20:03:45 +01:00
KatKatKateryna 869629e2a3 return var for unchanged classes 2023-06-02 19:31:35 +01:00
KatKatKateryna 48b98294fb remove constant var from a child class 2023-06-02 19:23:03 +01:00
KatKatKateryna 8205180e5d _untracked_receive referenced from core function 2023-06-02 13:27:25 +01:00
KatKatKateryna a2fd21f541 Metrics renamed to SDK Actions 2023-06-02 13:18:20 +01:00
KatKatKateryna 08ac76cf09 untracked API to Core 2023-06-01 23:48:04 +01:00
Gergő Jedlicska fbf19420fa Merge pull request #278 from specklesystems/gergo/structural_shape_type
fix(structural-properties): add missing ShapeType member
2023-05-30 18:11:42 +02:00
Gergő Jedlicska 44336addaf fix(structural-properties): add missing ShapeType member 2023-05-30 18:02:25 +02:00
ciga2011 43c9a9cace Update geometry.py
Pointcloud properties should be chunkable.
2023-05-11 16:07:58 +08:00
KatKatKateryna 99e9f773b8 Merge pull request #269 from specklesystems/objects/collections
Added collection object
2023-05-05 23:25:56 +08:00
KatKatKateryna 189a5847cf Update other.py 2023-05-05 15:12:00 +01:00
KatKatKateryna eb86b4881a Merge branch 'main' into objects/collections 2023-05-05 14:49:26 +01:00
KatKatKateryna 64fca5f280 Collections moved to Core 2023-05-05 14:48:26 +01:00
Morten Engen 784e9c1326 batch_sender: limit number of objects per batch 2023-04-18 15:13:39 +02:00
Gergő Jedlicska c7f5e0718b Merge pull request #267 from RobClaessensRHDHV/feature/suppress_type_error_during_type_check
Fix: also suppress TypeError during type checking.
2023-04-06 19:00:08 +02:00
908599 d2685c5cf5 Add unit tests for type validation of Union with float and dict. 2023-04-06 17:28:55 +02:00
908599 1b83e5a84b Merge branch 'main' of https://github.com/RobClaessensRHDHV/specklepy into feature/suppress_type_error_during_type_check 2023-04-06 17:10:18 +02:00
Gergő Jedlicska 77e09b9780 Merge pull request #266 from specklesystems/jrm/one-installer-for-all
Added Installer
2023-04-06 17:03:15 +02:00
Gergő Jedlicska 402f750200 Merge pull request #270 from specklesystems/gergo/fix_tests
gergo/fix tests
2023-04-06 17:02:11 +02:00
Gergő Jedlicska a43e7471a4 style(formatting): fix formatting issues 2023-04-06 16:57:44 +02:00
Gergő Jedlicska a4ed7ebb08 fix(integration-tests): fix integration tests 2023-04-06 16:56:21 +02:00
Jedd Morgan e7eb7c67a9 Added collection object 2023-04-06 14:14:29 +01:00
908599 d547cdaac0 Fix to also suppress TypeError during type checking.
This currently gives an error when float conversion is attempted with a non-numeric value.
2023-04-06 10:13:19 +02:00
Jedd Morgan 6f35c1bd20 Added Installer 2023-03-28 17:04:46 +01:00
Jedd Morgan 420c73f484 Merge pull request #265 from specklesystems/gergo/connector_install_path
feat(path-provider): add connector installation path helper
2023-03-28 13:47:56 +01:00
Gergő Jedlicska c2859475cc feat(path-provider): add connector installation path helper 2023-03-28 13:31:57 +02:00
Gergő Jedlicska 56e8d65e2b Merge pull request #264 from specklesystems/jsdbroughton-patch-1
fix: `invite_batch` failing where `emails` or `user_ids` is not provided
2023-03-22 18:32:41 +01:00
Jonathon Broughton 7885a6be8d fix: NoneType is not iterable 2023-03-21 17:46:16 +00:00
Jedd Morgan b19b85c9d1 Merge pull request #263 from specklesystems/jrm/instances
Updated instances to match sharp 2.13 changes
2023-03-21 13:12:50 +00:00
Jedd Morgan db4b2b7f87 Removed new collections class as we are not ready 2023-03-20 15:16:03 +00:00
Jedd Morgan 77916995bc Updated instances to match sharp 2.13 changes 2023-03-19 03:11:10 +00:00
Gergő Jedlicska 3dd56dc38e Merge pull request #260 from specklesystems/gergo/forward_ref_type
gergo/forward ref type
2023-02-15 20:48:55 +01:00
Gergő Jedlicska ae42bec1c3 style(formatting): rerun formatting 2023-02-15 19:21:48 +01:00
Gergő Jedlicska ea7baf8eb5 fix(type_checking): make sure forwardrefs blank pass type checking 2023-02-15 19:20:45 +01:00
Gergő Jedlicska 8352bb5c9a Merge pull request #257 from Knuttatutta/knuttatutta/add_axis_type
Fix: Add AxisType and fix its type in Axis
2023-02-01 15:27:34 +02:00
Gergő Jedlicska fc34b876fd Merge branch 'main' into knuttatutta/add_axis_type 2023-02-01 15:26:10 +02:00
Gergő Jedlicska 183993cfc5 Merge pull request #254 from RobClaessensRHDHV/feature/revit_analytical_to_specklepy_structural_fixes
Fix: Interoperability from Revit analytical to specklepy structural
2023-02-01 15:24:45 +02:00
Knuttatutta 9be3b4b93d Added AxisType in module init 2023-02-01 10:34:01 +01:00
Knuttatutta 0b14660115 Add AxisType, fix type in Axis 2023-02-01 10:16:13 +01:00
908599 68c4c682a0 Revert incorrect type hint change 2023-01-17 10:46:31 +01:00
908599 4f93ddcaf3 Update speckle_type of SectionProfile to match C#, update type hints with C# Enum values to int.
For the SectionProfile, a separate static variable e.g. STRUCTURAL_PROFILE could be created later, I now just made an easy fix.
For the enumeration attributes, I believe they should be integer, as e.g. Revit pushes them as integers, not strings.
2023-01-16 16:01:33 +01:00
Gergő Jedlicska e842f651b9 Merge pull request #253 from RobClaessensRHDHV/feature/structural_property_bugfix
Typo fix in properties.py
2023-01-16 13:53:44 +01:00
908599 7e1bec1aba Typo fix in properties.py 2023-01-16 11:39:41 +01:00
45 changed files with 3042 additions and 1500 deletions
+23 -33
View File
@@ -6,48 +6,38 @@ orbs:
jobs:
test:
docker:
- image: "cimg/python:<<parameters.tag>>"
- image: "cimg/node:16.15"
- image: "cimg/redis:6.2"
- image: "cimg/postgres:14.2"
environment:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
- image: "speckle/speckle-server"
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "127.0.0.1"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle2_test"
REDIS_URL: "redis://127.0.0.1"
SESSION_SECRET: "keyboard cat"
STRATEGY_LOCAL: "true"
CANONICAL_URL: "http://localhost:3000"
WAIT_HOSTS: 127.0.0.1:5432, 127.0.0.1:6379
DISABLE_FILE_UPLOADS: "true"
machine:
image: ubuntu-2204:2023.02.1
docker_layer_caching: true
resource_class: medium
parameters:
tag:
default: "3.8"
default: "3.11"
type: string
steps:
- checkout
- run: python --version
- run:
command: python -m pip install --upgrade pip
name: upgrade pip
- python/install-packages:
pkg-manager: poetry
- run: poetry run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
name: Install python
command: |
pyenv install -s << parameters.tag >>
pyenv global << parameters.tag >>
- run:
name: Startup the Speckle Server
command: docker compose -f docker-compose.yml up -d
- run:
name: Install Poetry
command: |
pip install poetry
- run:
name: Install packages
command: poetry install
- run:
name: Run tests
command: poetry run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
- store_test_results:
path: reports
- store_artifacts:
path: reports
- codecov/upload
deploy:
@@ -65,7 +55,7 @@ workflows:
- test:
matrix:
parameters:
tag: ["3.7", "3.8", "3.9", "3.10", "3.11"]
tag: ["3.11"]
filters:
tags:
only: /.*/
-12
View File
@@ -1,12 +0,0 @@
name: Update issue Status
on:
issues:
types: [closed]
jobs:
update_issue:
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
-12
View File
@@ -1,12 +0,0 @@
name: Move new issues into Project
on:
issues:
types: [opened]
jobs:
track_issue:
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
+3 -2
View File
@@ -5,6 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
@@ -17,8 +18,8 @@
"name": "Pytest",
"type": "python",
"request": "launch",
"program": "poetry",
"args": ["run", "pytest"],
"program": "pytest",
"args": [],
"console": "integratedTerminal",
"justMyCode": true
}
+162
View File
@@ -0,0 +1,162 @@
version: "3.9"
name: "speckle-server"
services:
####
# Speckle Server dependencies
#######
postgres:
image: "postgres:14.5-alpine"
restart: always
environment:
POSTGRES_DB: speckle
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- postgres-data:/var/lib/postgresql/data/
healthcheck:
# the -U user has to match the POSTGRES_USER value
test: ["CMD-SHELL", "pg_isready -U speckle"]
interval: 5s
timeout: 5s
retries: 30
redis:
image: "redis:6.0-alpine"
restart: always
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 5s
timeout: 5s
retries: 30
minio:
image: "minio/minio"
command: server /data --console-address ":9001"
restart: always
volumes:
- minio-data:/data
healthcheck:
test:
[
"CMD-SHELL",
"curl -s -o /dev/null http://127.0.0.1:9000/minio/index.html",
]
interval: 5s
timeout: 30s
retries: 30
start_period: 10s
####
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend:2
restart: always
ports:
- "0.0.0.0:8080:8080"
environment:
FILE_SIZE_LIMIT_MB: 100
speckle-server:
image: speckle/speckle-server:2
restart: always
healthcheck:
test:
[
"CMD",
"node",
"-e",
"require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/graphql?query={serverInfo{version}}', method: 'GET' }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(res.statusCode != 200 || body.toLowerCase().includes('error'));}); }).end();",
]
interval: 10s
timeout: 3s
retries: 30
ports:
- "0.0.0.0:3000:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
environment:
# TODO: Change this to the URL of the speckle server, as accessed from the network
CANONICAL_URL: "http://127.0.0.1:8080"
SPECKLE_AUTOMATE_URL: "http://127.0.0.1:3030"
# TODO: Change thvolumes:
REDIS_URL: "redis://redis"
S3_ENDPOINT: "http://minio:9000"
S3_ACCESS_KEY: "minioadmin"
S3_SECRET_KEY: "minioadmin"
S3_BUCKET: "speckle-server"
S3_CREATE_BUCKET: "true"
FILE_SIZE_LIMIT_MB: 100
# TODO: Change this to a unique secret for this server
SESSION_SECRET: "TODO:ReplaceWithLongString"
STRATEGY_LOCAL: "true"
DEBUG: "speckle:*"
POSTGRES_URL: "postgres"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
preview-service:
image: speckle/speckle-preview-service:2
restart: always
depends_on:
speckle-server:
condition: service_healthy
mem_limit: "1000m"
memswap_limit: "1000m"
environment:
DEBUG: "preview-service:*"
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle"
webhook-service:
image: speckle/speckle-webhook-service:2
restart: always
depends_on:
speckle-server:
condition: service_healthy
environment:
DEBUG: "webhook-service:*"
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle"
WAIT_HOSTS: postgres:5432
fileimport-service:
image: speckle/speckle-fileimport-service:2
restart: always
depends_on:
speckle-server:
condition: service_healthy
environment:
DEBUG: "fileimport-service:*"
PG_CONNECTION_STRING: "postgres://speckle:speckle@postgres/speckle"
WAIT_HOSTS: postgres:5432
S3_ENDPOINT: "http://minio:9000"
S3_ACCESS_KEY: "minioadmin"
S3_SECRET_KEY: "minioadmin"
S3_BUCKET: "speckle-server"
SPECKLE_SERVER_URL: "http://speckle-server:3000"
networks:
default:
name: speckle-server
volumes:
postgres-data:
redis-data:
minio-data:
+18 -11
View File
@@ -4,11 +4,12 @@ from warnings import warn
from deprecated import deprecated
from gql import Client
from gql.transport.exceptions import TransportServerError
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.websockets import WebsocketsTransport
from specklepy.api import resources
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.api.resources import (
active_user,
branch,
@@ -59,7 +60,6 @@ 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"
@@ -128,7 +128,6 @@ class SpeckleClient:
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:
@@ -140,12 +139,10 @@ class SpeckleClient:
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.account.token}",
"Content-Type": "application/json",
@@ -164,13 +161,23 @@ class SpeckleClient:
self._init_resources()
if self.user.get() is None:
warn(
SpeckleWarning(
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
try:
user_or_error = self.active_user.get()
if isinstance(user_or_error, SpeckleException):
if isinstance(user_or_error.exception, TransportServerError):
raise user_or_error.exception
else:
raise user_or_error
except TransportServerError as ex:
if ex.code == 403:
warn(
SpeckleWarning(
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
)
)
)
else:
raise ex
def execute_query(self, query: str) -> Dict:
return self.httpclient.execute(query)
+10 -108
View File
@@ -9,37 +9,11 @@ from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
class UserInfo(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
company: Optional[str] = None
id: Optional[str] = None
class Account(BaseModel):
isDefault: bool = False
token: Optional[str] = None
refreshToken: Optional[str] = None
serverInfo: ServerInfo = Field(default_factory=ServerInfo)
userInfo: UserInfo = Field(default_factory=UserInfo)
id: Optional[str] = None
def __repr__(self) -> str:
return (
f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url},"
f" isDefault: {self.isDefault})"
)
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
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.credentials import (UserInfo, Account, StreamWrapper,
get_account_from_token,
get_local_accounts as core_get_local_accounts)
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
"""Gets all the accounts present in this environment
@@ -51,53 +25,19 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
List[Account] -- list of all local accounts or an empty list if
no accounts were found
"""
accounts: List[Account] = []
try:
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
res = account_storage.get_all_objects()
account_storage.close()
if res:
accounts.extend(Account.parse_raw(r[1]) for r in res)
except SpeckleException:
# cannot open SQLiteTransport, probably because of the lack
# of disk write permissions
pass
json_acct_files = []
json_path = str(speckle_path_provider.accounts_folder_path())
try:
os.makedirs(json_path, exist_ok=True)
json_acct_files.extend(
file for file in os.listdir(json_path) if file.endswith(".json")
)
except Exception:
# cannot find or get the json account paths
pass
if json_acct_files:
try:
accounts.extend(
Account.parse_file(os.path.join(json_path, json_file))
for json_file in json_acct_files
)
except Exception as ex:
raise SpeckleException(
"Invalid json accounts could not be read. Please fix or remove them.",
ex,
) from ex
accounts = core_get_local_accounts(base_path)
metrics.track(
metrics.ACCOUNTS,
metrics.SDK,
next(
(acc for acc in accounts if acc.isDefault),
accounts[0] if accounts else None,
),
{"name": "Get Local Accounts"}
)
return accounts
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
"""
Gets this environment's default account if any. If there is no default,
@@ -108,7 +48,7 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
Returns:
Account -- the default account or None if no local accounts were found
"""
accounts = get_local_accounts(base_path=base_path)
accounts = core_get_local_accounts(base_path=base_path)
if not accounts:
return None
@@ -119,42 +59,4 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
metrics.initialise_tracker(default)
return default
def get_account_from_token(token: str, server_url: str = None) -> Account:
"""Gets the local account for the token if it exists
Arguments:
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
"""
accounts = get_local_accounts()
if not accounts:
return Account.from_token(token, server_url)
acct = next((acc for acc in accounts if acc.token == token), None)
if acct:
return acct
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 Account.from_token(token, server_url)
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(),
)
+12 -61
View File
@@ -7,6 +7,11 @@ from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.core.api.operations import (send as core_send,
receive as _untracked_receive,
serialize as core_serialize,
deserialize as core_deserialize)
def send(
base: Base,
@@ -24,31 +29,13 @@ def send(
Returns:
str -- the object id of the sent object
"""
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 isinstance(transports, AbstractTransport):
transports = [transports]
obj_hash = core_send(base, transports, use_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())
serializer = BaseObjectSerializer(write_transports=transports)
obj_hash, _ = serializer.write_json(base=base)
return obj_hash
@@ -56,15 +43,6 @@ def receive(
obj_id: str,
remote_transport: Optional[AbstractTransport] = None,
local_transport: Optional[AbstractTransport] = None,
) -> Base:
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
return _untracked_receive(obj_id, remote_transport, local_transport)
def _untracked_receive(
obj_id: str,
remote_transport: Optional[AbstractTransport] = None,
local_transport: Optional[AbstractTransport] = None,
) -> Base:
"""Receives an object from a transport.
@@ -77,29 +55,8 @@ def _untracked_receive(
Returns:
Base -- the base object
"""
if not local_transport:
local_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=local_transport)
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport
obj_string = local_transport.get_object(obj_id)
if obj_string:
return serializer.read_json(obj_string=obj_string)
if not remote_transport:
raise SpeckleException(
message=(
"Could not find the specified object using the local transport, and you"
" didn't provide a fallback remote from which to pull it."
)
)
obj_string = remote_transport.copy_object_and_children(
id=obj_id, target_transport=local_transport
)
return serializer.read_json(obj_string=obj_string)
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
return _untracked_receive(obj_id, remote_transport, local_transport)
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
@@ -116,11 +73,9 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
Returns:
str -- the serialized object
"""
metrics.track(metrics.SERIALIZE)
serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1]
metrics.track(metrics.SDK, custom_props={"name": "Serialize"})
return core_serialize(base, write_transports)
def deserialize(
obj_string: str, read_transport: Optional[AbstractTransport] = None
@@ -141,13 +96,9 @@ def deserialize(
Returns:
Base -- the deserialized object
"""
metrics.track(metrics.DESERIALIZE)
if not read_transport:
read_transport = SQLiteTransport()
metrics.track(metrics.SDK, custom_props={"name": "Deserialize"})
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
return core_deserialize(obj_string, read_transport)
__all__ = ["receive", "send", "serialize", "deserialize"]
+5 -2
View File
@@ -1,10 +1,11 @@
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from gql.client import Client
from gql.transport.exceptions import TransportQueryError
from graphql import DocumentNode
from specklepy.api.credentials import Account
from specklepy.core.api.credentials import Account
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
@@ -29,6 +30,7 @@ class ResourceBase(object):
self.name = name
self.server_version = server_version
self.schema: Optional[Type] = None
self.__lock = Lock()
def _step_into_response(self, response: dict, return_type: Union[str, List, None]):
"""Step into the dict to get the relevant data"""
@@ -68,7 +70,8 @@ class ResourceBase(object):
) -> Any:
"""Executes the GraphQL query"""
try:
response = self.client.execute(query, variable_values=params)
with self.__lock:
response = self.client.execute(query, variable_values=params)
except Exception as ex:
if isinstance(ex, TransportQueryError):
return GraphQLException(
+14 -189
View File
@@ -8,10 +8,10 @@ from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
NAME = "active_user"
from specklepy.core.api.resources.active_user import NAME, Resource as Core_Resource
class Resource(ResourceBase):
class Resource(Core_Resource):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
@@ -19,7 +19,6 @@ class Resource(ResourceBase):
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = User
@@ -35,28 +34,9 @@ class Resource(ResourceBase):
Returns:
User -- the retrieved user
"""
metrics.track(metrics.USER, self.account, {"name": "get"})
query = gql(
"""
query User {
activeUser {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
"""
)
params = {}
return self.make_request(query=query, params=params, return_type="activeUser")
metrics.track(metrics.SDK, custom_props={"name": "User Get"})
return super().get()
def update(
self,
@@ -76,106 +56,9 @@ class Resource(ResourceBase):
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.USER, self.account, {"name": "update"})
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
userUpdate(user: $user)
}
"""
)
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
params = {"user": {k: v for k, v in params.items() if v is not None}}
if not params["user"]:
return SpeckleException(
message=(
"You must provide at least one field to update your user profile"
)
)
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's
activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
activeUser {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["activeUser", "activity"],
schema=ActivityCollection,
)
metrics.track(metrics.SDK, self.account, {"name": "User Update"})
return super().update(name, company, bio, avatar)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
@@ -186,36 +69,9 @@ class Resource(ResourceBase):
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvites {
streamInvites{
id
token
inviteId
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
return self.make_request(
query=query,
return_type="streamInvites",
schema=PendingStreamCollaborator,
)
metrics.track(metrics.SDK, self.account, {"name": "Invite Get"})
return super().get_all_pending_invites()
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
@@ -233,37 +89,6 @@ class Resource(ResourceBase):
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
"""
query StreamInvite($streamId: String!, $token: String) {
streamInvite(streamId: $streamId, token: $token) {
id
token
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
params = {"streamId": stream_id}
if token:
params["token"] = token
return self.make_request(
query=query,
params=params,
return_type="streamInvite",
schema=PendingStreamCollaborator,
)
metrics.track(metrics.SDK, self.account, {"name": "Invite Get"})
return super().get_pending_invite(stream_id, token)
+16 -138
View File
@@ -6,10 +6,10 @@ from specklepy.api.models import Branch
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
NAME = "branch"
from specklepy.core.api.resources.branch import NAME, Resource as Core_Resource
class Resource(ResourceBase):
class Resource(Core_Resource):
"""API Access class for branches"""
def __init__(self, account, basepath, client) -> None:
@@ -17,7 +17,6 @@ class Resource(ResourceBase):
account=account,
basepath=basepath,
client=client,
name=NAME,
)
self.schema = Branch
@@ -33,25 +32,9 @@ 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!) {
branchCreate(branch: $branch)
}
"""
)
params = {
"branch": {
"streamId": stream_id,
"name": name,
"description": description,
}
}
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
return self.make_request(
query=query, params=params, return_type="branchCreate", parse_response=False
)
return super().create(stream_id, name, description)
def get(self, stream_id: str, name: str, commits_limit: int = 10):
"""Get a branch by name from a stream
@@ -64,42 +47,9 @@ 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!) {
stream(id: $stream_id) {
branch(name: $name) {
id,
name,
description,
commits (limit: $commits_limit) {
totalCount,
cursor,
items {
id,
referencedObject,
sourceApplication,
totalChildrenCount,
message,
authorName,
authorId,
branchName,
parents,
createdAt
}
}
}
}
}
"""
)
params = {"stream_id": stream_id, "name": name, "commits_limit": commits_limit}
return self.make_request(
query=query, params=params, return_type=["stream", "branch"]
)
metrics.track(metrics.SDK, self.account, {"name": "Branch Get"})
return super().get(stream_id, name, commits_limit)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
@@ -112,50 +62,9 @@ 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!
) {
stream(id: $stream_id) {
branches(limit: $branches_limit) {
items {
id
name
description
commits(limit: $commits_limit) {
totalCount
items{
id
message
referencedObject
sourceApplication
parents
authorId
authorName
branchName
createdAt
}
}
}
}
}
}
"""
)
params = {
"stream_id": stream_id,
"branches_limit": branches_limit,
"commits_limit": commits_limit,
}
return self.make_request(
query=query, params=params, return_type=["stream", "branches", "items"]
)
metrics.track(metrics.SDK, self.account, {"name": "Branch Get"})
return super().list(stream_id, branches_limit, commits_limit)
def update(
self,
@@ -175,29 +84,9 @@ class Resource(ResourceBase):
Returns:
bool -- True if update is successful
"""
metrics.track(metrics.BRANCH, self.account, {"name": "update"})
query = gql(
"""
mutation BranchUpdate($branch: BranchUpdateInput!) {
branchUpdate(branch: $branch)
}
"""
)
params = {
"branch": {
"streamId": stream_id,
"id": branch_id,
}
}
if name:
params["branch"]["name"] = name
if description:
params["branch"]["description"] = description
return self.make_request(
query=query, params=params, return_type="branchUpdate", parse_response=False
)
metrics.track(metrics.SDK, self.account, {"name": "Branch Update"})
return super().update(stream_id, branch_id, name, description)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
@@ -209,17 +98,6 @@ class Resource(ResourceBase):
Returns:
bool -- True if deletion is successful
"""
metrics.track(metrics.BRANCH, self.account, {"name": "delete"})
query = gql(
"""
mutation BranchDelete($branch: BranchDeleteInput!) {
branchDelete(branch: $branch)
}
"""
)
params = {"branch": {"streamId": stream_id, "id": branch_id}}
return self.make_request(
query=query, params=params, return_type="branchDelete", parse_response=False
)
metrics.track(metrics.SDK, self.account, {"name": "Branch Delete"})
return super().delete(stream_id, branch_id)
+20 -134
View File
@@ -6,10 +6,10 @@ from specklepy.api.models import Commit
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
NAME = "commit"
from specklepy.core.api.resources.commit import NAME, Resource as Core_Resource
class Resource(ResourceBase):
class Resource(Core_Resource):
"""API Access class for commits"""
def __init__(self, account, basepath, client) -> None:
@@ -17,7 +17,6 @@ class Resource(ResourceBase):
account=account,
basepath=basepath,
client=client,
name=NAME,
)
self.schema = Commit
@@ -32,32 +31,9 @@ class Resource(ResourceBase):
Returns:
Commit -- the retrieved commit object
"""
query = gql(
"""
query Commit($stream_id: String!, $commit_id: String!) {
stream(id: $stream_id) {
commit(id: $commit_id) {
id
message
referencedObject
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
parents
}
}
}
"""
)
params = {"stream_id": stream_id, "commit_id": commit_id}
return self.make_request(
query=query, params=params, return_type=["stream", "commit"]
)
metrics.track(metrics.SDK, self.account, {"name": "Commit Get"})
return super().get(stream_id, commit_id)
def list(self, stream_id: str, limit: int = 10) -> List[Commit]:
"""
@@ -70,36 +46,9 @@ 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!) {
stream(id: $stream_id) {
commits(limit: $limit) {
items {
id
message
referencedObject
authorName
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
parents
}
}
}
}
"""
)
params = {"stream_id": stream_id, "limit": limit}
return self.make_request(
query=query, params=params, return_type=["stream", "commits", "items"]
)
metrics.track(metrics.SDK, self.account, {"name": "Commit Get"})
return super().list(stream_id, limit)
def create(
self,
@@ -128,28 +77,9 @@ 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)}
"""
)
params = {
"commit": {
"streamId": stream_id,
"branchName": branch_name,
"objectId": object_id,
"message": message,
"sourceApplication": source_application,
}
}
if parents:
params["commit"]["parents"] = parents
return self.make_request(
query=query, params=params, return_type="commitCreate", parse_response=False
)
metrics.track(metrics.SDK, self.account, {"name": "Commit Create"})
return super().create(stream_id, object_id, branch_name, message, source_application, parents)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
@@ -164,20 +94,9 @@ 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)}
"""
)
params = {
"commit": {"streamId": stream_id, "id": commit_id, "message": message}
}
return self.make_request(
query=query, params=params, return_type="commitUpdate", parse_response=False
)
metrics.track(metrics.SDK, self.account, {"name": "Commit Update"})
return super().update(stream_id, commit_id, message)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
@@ -191,18 +110,9 @@ 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)}
"""
)
params = {"commit": {"streamId": stream_id, "id": commit_id}}
return self.make_request(
query=query, params=params, return_type="commitDelete", parse_response=False
)
metrics.track(metrics.SDK, self.account, {"name": "Commit Delete"})
return super().delete(stream_id, commit_id)
def received(
self,
@@ -214,30 +124,6 @@ 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!){
commitReceive(input:$receivedInput)
}
"""
)
params = {
"receivedInput": {
"sourceApplication": source_application,
"streamId": stream_id,
"commitId": commit_id,
"message": "message",
}
}
try:
return self.make_request(
query=query,
params=params,
return_type="commitReceive",
parse_response=False,
)
except Exception as ex:
print(ex.with_traceback)
return False
metrics.track(metrics.SDK, self.account, {"name": "Commit Received"})
return super().received(stream_id, commit_id, source_application, message)
+8 -123
View File
@@ -8,10 +8,10 @@ from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
NAME = "other_user"
from specklepy.core.api.resources.other_user import NAME, Resource as Core_Resource
class Resource(ResourceBase):
class Resource(Core_Resource):
"""API Access class for other users, that are not the currently active user."""
def __init__(self, account, basepath, client, server_version) -> None:
@@ -19,7 +19,6 @@ class Resource(ResourceBase):
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = LimitedUser
@@ -34,26 +33,9 @@ class Resource(ResourceBase):
Returns:
LimitedUser -- the retrieved profile of another user
"""
metrics.track(metrics.OTHER_USER, self.account, {"name": "get"})
query = gql(
"""
query OtherUser($id: String!) {
otherUser(id: $id) {
id
name
bio
company
avatar
verified
role
}
}
"""
)
params = {"id": id}
return self.make_request(query=query, params=params, return_type="otherUser")
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
def search(
self, search_query: str, limit: int = 25
@@ -72,104 +54,7 @@ class Resource(ResourceBase):
message="User search query must be at least 3 characters"
)
metrics.track(metrics.OTHER_USER, self.account, {"name": "search"})
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
}
}
}
"""
)
params = {"search_query": search_query, "limit": limit}
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
return self.make_request(
query=query, params=params, return_type=["userSearch", "items"]
)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$user_id: String!,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
otherUser(id: $user_id) {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
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=["otherUser", "activity"],
schema=ActivityCollection,
)
+10 -119
View File
@@ -8,10 +8,10 @@ from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import GraphQLException
NAME = "server"
from specklepy.core.api.resources.server import NAME, Resource as Core_Resource
class Resource(ResourceBase):
class Resource(Core_Resource):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
@@ -19,7 +19,6 @@ class Resource(ResourceBase):
account=account,
basepath=basepath,
client=client,
name=NAME,
)
def get(self) -> ServerInfo:
@@ -28,72 +27,9 @@ class Resource(ResourceBase):
Returns:
dict -- the server info in dictionary form
"""
metrics.track(metrics.SERVER, self.account, {"name": "get"})
query = gql(
"""
query Server {
serverInfo {
name
company
description
adminContact
canonicalUrl
version
roles {
name
description
resourceTarget
}
scopes {
name
description
}
authStrategies{
id
name
icon
}
}
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Server Get"})
return self.make_request(
query=query, return_type="serverInfo", schema=ServerInfo
)
def version(self) -> Tuple[Any, ...]:
"""Get the server version
Returns:
the server version in the format (major, minor, patch, (tag, build))
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
"""
# not tracking as it will be called along with other mutations / queries as a check
query = gql(
"""
query Server {
serverInfo {
version
}
}
"""
)
ver = self.make_request(
query=query, return_type=["serverInfo", "version"], parse_response=False
)
if isinstance(ver, Exception):
raise GraphQLException(
f"Could not get server version for {self.basepath}", [ver]
)
# pylint: disable=consider-using-generator; (list comp is faster)
return tuple(
[
int(segment) if segment.isdigit() else segment
for segment in re.split(r"\.|-", ver)
]
)
return super().get()
def apps(self) -> Dict:
"""Get the apps registered on the server
@@ -101,28 +37,9 @@ 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 {
apps{
id
name
description
termsAndConditionsLink
trustByDefault
logo
author {
id
name
avatar
}
}
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Server Apps"})
return self.make_request(query=query, return_type="apps", parse_response=False)
return super().apps()
def create_token(self, name: str, scopes: List[str], lifespan: int) -> str:
"""Create a personal API token
@@ -135,22 +52,9 @@ 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!) {
apiTokenCreate(token: $token)
}
"""
)
params = {"token": {"scopes": scopes, "name": name, "lifespan": lifespan}}
metrics.track(metrics.SDK, self.account, {"name": "Server Create Token"})
return self.make_request(
query=query,
params=params,
return_type="apiTokenCreate",
parse_response=False,
)
return super().create_token(name, scopes, lifespan)
def revoke_token(self, token: str) -> bool:
"""Revokes (deletes) a personal API token
@@ -161,19 +65,6 @@ 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!) {
apiTokenRevoke(token: $token)
}
"""
)
params = {"token": token}
metrics.track(metrics.SDK, self.account, {"name": "Server Revoke Token"})
return self.make_request(
query=query,
params=params,
return_type="apiTokenRevoke",
parse_response=False,
)
return super().revoke_token(token)
+31 -510
View File
@@ -9,10 +9,10 @@ from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
NAME = "stream"
from specklepy.core.api.resources.stream import NAME, Resource as Core_Resource
class Resource(ResourceBase):
class Resource(Core_Resource):
"""API Access class for streams"""
def __init__(self, account, basepath, client, server_version) -> None:
@@ -20,7 +20,6 @@ class Resource(ResourceBase):
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
@@ -37,56 +36,9 @@ class Resource(ResourceBase):
Returns:
Stream -- the retrieved stream
"""
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
role
description
isPublic
createdAt
updatedAt
commentCount
favoritesCount
collaborators {
id
name
role
avatar
}
branches(limit: $branch_limit) {
totalCount
cursor
items {
id
name
description
commits(limit: $commit_limit) {
totalCount
cursor
items {
id
message
authorId
createdAt
authorName
referencedObject
sourceApplication
}
}
}
}
}
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Get"})
params = {"id": id, "branch_limit": branch_limit, "commit_limit": commit_limit}
return self.make_request(query=query, params=params, return_type="stream")
return super().get(id, branch_limit, commit_limit)
def list(self, stream_limit: int = 10) -> List[Stream]:
"""Get a list of the user's streams
@@ -97,50 +49,9 @@ class Resource(ResourceBase):
Returns:
List[Stream] -- A list of Stream objects
"""
metrics.track(metrics.STREAM, self.account, {"name": "get"})
query = gql(
"""
query User($stream_limit: Int!) {
user {
id
bio
name
email
avatar
company
verified
profiles
role
streams(limit: $stream_limit) {
totalCount
cursor
items {
id
name
role
isPublic
createdAt
updatedAt
description
commentCount
favoritesCount
collaborators {
id
name
role
}
}
}
}
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Get"})
params = {"stream_limit": stream_limit}
return self.make_request(
query=query, params=params, return_type=["user", "streams", "items"]
)
return super().list(stream_limit)
def create(
self,
@@ -159,22 +70,9 @@ class Resource(ResourceBase):
Returns:
id {str} -- the id of the newly created stream
"""
metrics.track(metrics.STREAM, self.account, {"name": "create"})
query = gql(
"""
mutation StreamCreate($stream: StreamCreateInput!) {
streamCreate(stream: $stream)
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Create"})
params = {
"stream": {"name": name, "description": description, "isPublic": is_public}
}
return self.make_request(
query=query, params=params, return_type="streamCreate", parse_response=False
)
return super().create(name, description, is_public)
def update(
self,
@@ -195,27 +93,9 @@ class Resource(ResourceBase):
Returns:
bool -- whether the stream update was successful
"""
metrics.track(metrics.STREAM, self.account, {"name": "update"})
query = gql(
"""
mutation StreamUpdate($stream: StreamUpdateInput!) {
streamUpdate(stream: $stream)
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Update"})
params = {
"id": id,
"name": name,
"description": description,
"isPublic": is_public,
}
# remove None values so graphql doesn't cry
params = {"stream": {k: v for k, v in params.items() if v is not None}}
return self.make_request(
query=query, params=params, return_type="streamUpdate", parse_response=False
)
return super().update(id, name, description, is_public)
def delete(self, id: str) -> bool:
"""Delete a stream given its id
@@ -226,20 +106,9 @@ class Resource(ResourceBase):
Returns:
bool -- whether the deletion was successful
"""
metrics.track(metrics.STREAM, self.account, {"name": "delete"})
query = gql(
"""
mutation StreamDelete($id: String!) {
streamDelete(id: $id)
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Delete"})
params = {"id": id}
return self.make_request(
query=query, params=params, return_type="streamDelete", parse_response=False
)
return super().delete(id)
def search(
self,
@@ -259,67 +128,9 @@ class Resource(ResourceBase):
Returns:
List[Stream] -- a list of Streams that match the search query
"""
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
role
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
}
}
}
}
}
}
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Search"})
params = {
"search_query": search_query,
"limit": limit,
"branch_limit": branch_limit,
"commit_limit": commit_limit,
}
return self.make_request(
query=query, params=params, return_type=["streams", "items"]
)
return super().search(search_query, limit, branch_limit, commit_limit)
def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream.
@@ -332,28 +143,9 @@ class Resource(ResourceBase):
Returns:
Stream -- the stream with its `id`, `name`, and `favoritedDate`
"""
metrics.track(metrics.STREAM, self.account, {"name": "favorite"})
query = gql(
"""
mutation StreamFavorite($stream_id: String!, $favorited: Boolean!) {
streamFavorite(streamId: $stream_id, favorited: $favorited) {
id
name
favoritedDate
favoritesCount
}
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Favorite"})
params = {
"stream_id": stream_id,
"favorited": favorited,
}
return self.make_request(
query=query, params=params, return_type=["streamFavorite"]
)
return super().favorite(stream_id, favorited)
@deprecated(
version="2.6.4",
@@ -375,7 +167,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role})
#metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role})
# we're checking for the actual version info, and if the version is 'dev' we treat it
# as an up to date instance
if self.server_version and (
@@ -428,46 +220,9 @@ class Resource(ResourceBase):
List[PendingStreamCollaborator]
-- a list of pending invites for the specified stream
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
metrics.track(metrics.SDK, self.account, {"name": "Invite Get"})
query = gql(
"""
query StreamInvites($streamId: String!) {
stream(id: $streamId){
pendingCollaborators {
id
token
inviteId
streamId
streamName
title
role
invitedBy{
id
name
company
avatar
}
user {
id
name
company
avatar
}
}
}
}
"""
)
params = {"streamId": stream_id}
return self.make_request(
query=query,
params=params,
return_type=["stream", "pendingCollaborators"],
schema=PendingStreamCollaborator,
)
return super().get_all_pending_invites(stream_id)
def invite(
self,
@@ -493,38 +248,9 @@ class Resource(ResourceBase):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.INVITE, self.account, {"name": "create"})
self._check_invites_supported()
metrics.track(metrics.SDK, self.account, {"name": "Invite Create"})
if email is None and user_id is None:
raise SpeckleException(
"You must provide either an email or a user id to use the"
" `stream.invite` method"
)
query = gql(
"""
mutation StreamInviteCreate($input: StreamInviteCreateInput!) {
streamInviteCreate(input: $input)
}
"""
)
params = {
"email": email,
"userId": user_id,
"streamId": stream_id,
"message": message,
"role": role,
}
params = {"input": {k: v for k, v in params.items() if v is not None}}
return self.make_request(
query=query,
params=params,
return_type="streamInviteCreate",
parse_response=False,
)
return super().invite(stream_id, email, user_id, role, message)
def invite_batch(
self,
@@ -549,42 +275,9 @@ class Resource(ResourceBase):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.INVITE, self.account, {"name": "batch create"})
self._check_invites_supported()
if emails is None and user_ids is None:
raise SpeckleException(
"You must provide either an email or a user id to use the"
" `stream.invite` method"
)
metrics.track(metrics.SDK, self.account, {"name": "Invite Batch Create"})
query = gql(
"""
mutation StreamInviteBatchCreate($input: [StreamInviteCreateInput!]!) {
streamInviteBatchCreate(input: $input)
}
"""
)
email_invites = [
{"streamId": stream_id, "message": message, "email": email}
for email in emails
if emails is not None
]
user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id}
for user_id in user_ids
if user_ids is not None
]
params = {"input": [*email_invites, *user_invites]}
return self.make_request(
query=query,
params=params,
return_type="streamInviteBatchCreate",
parse_response=False,
)
return super().invite_batch(stream_id, emails, user_ids, message)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite
@@ -598,25 +291,9 @@ class Resource(ResourceBase):
Returns:
bool -- true if the operation was successful
"""
metrics.track(metrics.INVITE, self.account, {"name": "cancel"})
self._check_invites_supported()
metrics.track(metrics.SDK, self.account, {"name": "Invite Cancel"})
query = gql(
"""
mutation StreamInviteCancel($streamId: String!, $inviteId: String!) {
streamInviteCancel(streamId: $streamId, inviteId: $inviteId)
}
"""
)
params = {"streamId": stream_id, "inviteId": invite_id}
return self.make_request(
query=query,
params=params,
return_type="streamInviteCancel",
parse_response=False,
)
return super().invite_cancel(stream_id, invite_id)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite
@@ -632,29 +309,9 @@ class Resource(ResourceBase):
Returns:
bool -- true if the operation was successful
"""
metrics.track(metrics.INVITE, self.account, {"name": "use"})
self._check_invites_supported()
metrics.track(metrics.SDK, self.account, {"name": "Invite Use"})
query = gql(
"""
mutation StreamInviteUse(
$accept: Boolean!,
$streamId: String!,
$token: String!
) {
streamInviteUse(accept: $accept, streamId: $streamId, token: $token)
}
"""
)
params = {"streamId": stream_id, "token": token, "accept": accept}
return self.make_request(
query=query,
params=params,
return_type="streamInviteUse",
parse_response=False,
)
return super().invite_use(stream_id, token, accept)
def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream
@@ -669,41 +326,9 @@ class Resource(ResourceBase):
Returns:
bool -- True if the operation was successful
"""
metrics.track(
metrics.PERMISSION, self.account, {"name": "update", "role": role}
)
if self.server_version and (
self.server_version != ("dev",) and self.server_version < (2, 6, 4)
):
raise UnsupportedException(
"Server mutation `update_permission` is only supported as of Speckle"
" Server v2.6.4. Please update your Speckle Server to use this method"
" or use the `grant_permission` method instead."
)
query = gql(
"""
mutation StreamUpdatePermission(
$permission_params: StreamUpdatePermissionInput!
) {
streamUpdatePermission(permissionParams: $permission_params)
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Permission Update", "role": role})
params = {
"permission_params": {
"streamId": stream_id,
"userId": user_id,
"role": role,
}
}
return self.make_request(
query=query,
params=params,
return_type="streamUpdatePermission",
parse_response=False,
)
return super().update_permission(stream_id, user_id, role)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
@@ -715,111 +340,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!
) {
streamRevokePermission(permissionParams: $permission_params)
}
"""
)
metrics.track(metrics.SDK, self.account, {"name": "Permission Revoke"})
params = {"permission_params": {"streamId": stream_id, "userId": user_id}}
return super().revoke_permission(stream_id, user_id)
return self.make_request(
query=query,
params=params,
return_type="streamRevokePermission",
parse_response=False,
)
def activity(
self,
stream_id: str,
action_type: Optional[str] = None,
limit: int = 20,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
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,
)
+5 -5
View File
@@ -44,7 +44,7 @@ class Resource(ResourceBase):
Returns:
User -- the retrieved user
"""
metrics.track(metrics.USER, self.account, {"name": "get"})
#metrics.track(metrics.USER, self.account, {"name": "get"})
query = gql(
"""
query User($id: String) {
@@ -86,7 +86,7 @@ class Resource(ResourceBase):
message="User search query must be at least 3 characters"
)
metrics.track(metrics.USER, self.account, {"name": "search"})
#metrics.track(metrics.USER, self.account, {"name": "search"})
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
@@ -128,7 +128,7 @@ class Resource(ResourceBase):
Returns:
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.USER, self.account, {"name": "update"})
#metrics.track(metrics.USER, self.account, {"name": "update"})
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
@@ -243,7 +243,7 @@ class Resource(ResourceBase):
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
#metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
@@ -291,7 +291,7 @@ class Resource(ResourceBase):
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.INVITE, self.account, {"name": "get"})
#metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
query = gql(
+1 -3
View File
@@ -2,12 +2,11 @@ from urllib.parse import unquote, urlparse
from warnings import warn
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import (
from specklepy.core.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
@@ -75,7 +74,6 @@ class StreamWrapper:
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(
View File
+152
View File
@@ -0,0 +1,152 @@
import os
from typing import List, Optional
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from specklepy.api.models import ServerInfo
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
class UserInfo(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
company: Optional[str] = None
id: Optional[str] = None
class Account(BaseModel):
isDefault: bool = False
token: Optional[str] = None
refreshToken: Optional[str] = None
serverInfo: ServerInfo = Field(default_factory=ServerInfo)
userInfo: UserInfo = Field(default_factory=UserInfo)
id: Optional[str] = None
def __repr__(self) -> str:
return (
f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url},"
f" isDefault: {self.isDefault})"
)
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: Optional[str] = None) -> List[Account]:
"""Gets all the accounts present in this environment
Arguments:
base_path {str} -- custom base path if you are not using the system default
Returns:
List[Account] -- list of all local accounts or an empty list if
no accounts were found
"""
accounts: List[Account] = []
try:
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
res = account_storage.get_all_objects()
account_storage.close()
if res:
accounts.extend(Account.parse_raw(r[1]) for r in res)
except SpeckleException:
# cannot open SQLiteTransport, probably because of the lack
# of disk write permissions
pass
json_acct_files = []
json_path = str(speckle_path_provider.accounts_folder_path())
try:
os.makedirs(json_path, exist_ok=True)
json_acct_files.extend(
file for file in os.listdir(json_path) if file.endswith(".json")
)
except Exception:
# cannot find or get the json account paths
pass
if json_acct_files:
try:
accounts.extend(
Account.parse_file(os.path.join(json_path, json_file))
for json_file in json_acct_files
)
except Exception as ex:
raise SpeckleException(
"Invalid json accounts could not be read. Please fix or remove them.",
ex,
) from ex
return accounts
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
"""
Gets this environment's default account if any. If there is no default,
the first found will be returned and set as default.
Arguments:
base_path {str} -- custom base path if you are not using the system default
Returns:
Account -- the default account or None if no local accounts were found
"""
accounts = get_local_accounts(base_path=base_path)
if not accounts:
return None
default = next((acc for acc in accounts if acc.isDefault), None)
if not default:
default = accounts[0]
default.isDefault = True
#metrics.initialise_tracker(default)
return default
def get_account_from_token(token: str, server_url: str = None) -> Account:
"""Gets the local account for the token if it exists
Arguments:
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
"""
accounts = get_local_accounts()
if not accounts:
return Account.from_token(token, server_url)
acct = next((acc for acc in accounts if acc.token == token), None)
if acct:
return acct
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 Account.from_token(token, server_url)
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(),
)
+139
View File
@@ -0,0 +1,139 @@
from typing import List, Optional
#from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.transports.sqlite import SQLiteTransport
def send(
base: Base,
transports: Optional[List[AbstractTransport]] = None,
use_default_cache: bool = True,
):
"""Sends an object via the provided transports. Defaults to the local cache.
Arguments:
obj {Base} -- the object you want to send
transports {list} -- where you want to send them
use_default_cache {bool} -- toggle for the default cache.
If set to false, it will only send to the provided transports
Returns:
str -- the object id of the sent object
"""
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 isinstance(transports, AbstractTransport):
transports = [transports]
if transports is None:
transports = []
if use_default_cache:
transports.insert(0, SQLiteTransport())
serializer = BaseObjectSerializer(write_transports=transports)
obj_hash, _ = serializer.write_json(base=base)
return obj_hash
def receive(
obj_id: str,
remote_transport: Optional[AbstractTransport] = None,
local_transport: Optional[AbstractTransport] = None,
) -> Base:
"""Receives an object from a transport.
Arguments:
obj_id {str} -- the id of the object to receive
remote_transport {Transport} -- the transport to receive from
local_transport {Transport} -- the local cache to check for existing objects
(defaults to `SQLiteTransport`)
Returns:
Base -- the base object
"""
if not local_transport:
local_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=local_transport)
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport
obj_string = local_transport.get_object(obj_id)
if obj_string:
return serializer.read_json(obj_string=obj_string)
if not remote_transport:
raise SpeckleException(
message=(
"Could not find the specified object using the local transport, and you"
" didn't provide a fallback remote from which to pull it."
)
)
obj_string = remote_transport.copy_object_and_children(
id=obj_id, target_transport=local_transport
)
return serializer.read_json(obj_string=obj_string)
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
"""
Serialize a base object. If no write transports are provided,
the object will be serialized
without detaching or chunking any of the attributes.
Arguments:
base {Base} -- the object to serialize
write_transports {List[AbstractTransport]}
-- optional: the transports to write to
Returns:
str -- the serialized object
"""
serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1]
def deserialize(
obj_string: str, read_transport: Optional[AbstractTransport] = None
) -> Base:
"""
Deserialize a string object into a Base object.
If the object contains referenced child objects that are not stored in the local db,
a read transport needs to be provided in order to recompose
the base with the children objects.
Arguments:
obj_string {str} -- the string object to deserialize
read_transport {AbstractTransport}
-- the transport to fetch children objects from
(defaults to SQLiteTransport)
Returns:
Base -- the deserialized object
"""
if not read_transport:
read_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
__all__ = ["receive", "send", "serialize", "deserialize"]
@@ -0,0 +1,264 @@
from datetime import datetime, timezone
from typing import List, Optional
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "active_user"
class Resource(ResourceBase):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = User
def get(self) -> User:
"""Gets the profile of a user. If no id argument is provided,
will return the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
query = gql(
"""
query User {
activeUser {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
"""
)
params = {}
return self.make_request(query=query, params=params, return_type="activeUser")
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
"""
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
userUpdate(user: $user)
}
"""
)
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
params = {"user": {k: v for k, v in params.items() if v is not None}}
if not params["user"]:
return SpeckleException(
message=(
"You must provide at least one field to update your user profile"
)
)
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's
activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
activeUser {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["activeUser", "activity"],
schema=ActivityCollection,
)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
self._check_invites_supported()
query = gql(
"""
query StreamInvites {
streamInvites{
id
token
inviteId
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
return self.make_request(
query=query,
return_type="streamInvites",
schema=PendingStreamCollaborator,
)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
self._check_invites_supported()
query = gql(
"""
query StreamInvite($streamId: String!, $token: String) {
streamInvite(streamId: $streamId, token: $token) {
id
token
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
params = {"streamId": stream_id}
if token:
params["token"] = token
return self.make_request(
query=query,
params=params,
return_type="streamInvite",
schema=PendingStreamCollaborator,
)
+219
View File
@@ -0,0 +1,219 @@
from typing import Optional
from gql import gql
from specklepy.api.models import Branch
from specklepy.api.resource import ResourceBase
NAME = "branch"
class Resource(ResourceBase):
"""API Access class for branches"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
)
self.schema = Branch
def create(
self, stream_id: str, name: str, description: str = "No description provided"
) -> str:
"""Create a new branch on this stream
Arguments:
name {str} -- the name of the new branch
description {str} -- a short description of the branch
Returns:
id {str} -- the newly created branch's id
"""
query = gql(
"""
mutation BranchCreate($branch: BranchCreateInput!) {
branchCreate(branch: $branch)
}
"""
)
params = {
"branch": {
"streamId": stream_id,
"name": name,
"description": description,
}
}
return self.make_request(
query=query, params=params, return_type="branchCreate", parse_response=False
)
def get(self, stream_id: str, name: str, commits_limit: int = 10):
"""Get a branch by name from a stream
Arguments:
stream_id {str} -- the id of the stream to get the branch from
name {str} -- the name of the branch to get
commits_limit {int} -- maximum number of commits to get
Returns:
Branch -- the fetched branch with its latest commits
"""
query = gql(
"""
query BranchGet($stream_id: String!, $name: String!, $commits_limit: Int!) {
stream(id: $stream_id) {
branch(name: $name) {
id,
name,
description,
commits (limit: $commits_limit) {
totalCount,
cursor,
items {
id,
referencedObject,
sourceApplication,
totalChildrenCount,
message,
authorName,
authorId,
branchName,
parents,
createdAt
}
}
}
}
}
"""
)
params = {"stream_id": stream_id, "name": name, "commits_limit": commits_limit}
return self.make_request(
query=query, params=params, return_type=["stream", "branch"]
)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
Arguments:
stream_id {str} -- the id of the stream to get the branches from
branches_limit {int} -- maximum number of branches to get
commits_limit {int} -- maximum number of commits to get
Returns:
List[Branch] -- the branches on the stream
"""
query = gql(
"""
query BranchesGet(
$stream_id: String!,
$branches_limit: Int!,
$commits_limit: Int!
) {
stream(id: $stream_id) {
branches(limit: $branches_limit) {
items {
id
name
description
commits(limit: $commits_limit) {
totalCount
items{
id
message
referencedObject
sourceApplication
parents
authorId
authorName
branchName
createdAt
}
}
}
}
}
}
"""
)
params = {
"stream_id": stream_id,
"branches_limit": branches_limit,
"commits_limit": commits_limit,
}
return self.make_request(
query=query, params=params, return_type=["stream", "branches", "items"]
)
def update(
self,
stream_id: str,
branch_id: str,
name: Optional[str] = None,
description: Optional[str] = None,
):
"""Update a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to update
branch_id {str} -- the id of the branch to update
name {str} -- optional: the updated branch name
description {str} -- optional: the updated branch description
Returns:
bool -- True if update is successful
"""
query = gql(
"""
mutation BranchUpdate($branch: BranchUpdateInput!) {
branchUpdate(branch: $branch)
}
"""
)
params = {
"branch": {
"streamId": stream_id,
"id": branch_id,
}
}
if name:
params["branch"]["name"] = name
if description:
params["branch"]["description"] = description
return self.make_request(
query=query, params=params, return_type="branchUpdate", parse_response=False
)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to delete
branch_id {str} -- the branch to delete
Returns:
bool -- True if deletion is successful
"""
query = gql(
"""
mutation BranchDelete($branch: BranchDeleteInput!) {
branchDelete(branch: $branch)
}
"""
)
params = {"branch": {"streamId": stream_id, "id": branch_id}}
return self.make_request(
query=query, params=params, return_type="branchDelete", parse_response=False
)
+237
View File
@@ -0,0 +1,237 @@
from typing import List, Optional
from gql import gql
from specklepy.api.models import Commit
from specklepy.api.resource import ResourceBase
NAME = "commit"
class Resource(ResourceBase):
"""API Access class for commits"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
)
self.schema = Commit
def get(self, stream_id: str, commit_id: str) -> Commit:
"""
Gets a commit given a stream and the commit id
Arguments:
stream_id {str} -- the stream where we can find the commit
commit_id {str} -- the id of the commit you want to get
Returns:
Commit -- the retrieved commit object
"""
query = gql(
"""
query Commit($stream_id: String!, $commit_id: String!) {
stream(id: $stream_id) {
commit(id: $commit_id) {
id
message
referencedObject
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
parents
}
}
}
"""
)
params = {"stream_id": stream_id, "commit_id": commit_id}
return self.make_request(
query=query, params=params, return_type=["stream", "commit"]
)
def list(self, stream_id: str, limit: int = 10) -> List[Commit]:
"""
Get a list of commits on a given stream
Arguments:
stream_id {str} -- the stream where the commits are
limit {int} -- the maximum number of commits to fetch (default = 10)
Returns:
List[Commit] -- a list of the most recent commit objects
"""
query = gql(
"""
query Commits($stream_id: String!, $limit: Int!) {
stream(id: $stream_id) {
commits(limit: $limit) {
items {
id
message
referencedObject
authorName
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
parents
}
}
}
}
"""
)
params = {"stream_id": stream_id, "limit": limit}
return self.make_request(
query=query, params=params, return_type=["stream", "commits", "items"]
)
def create(
self,
stream_id: str,
object_id: str,
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: List[str] = None,
) -> str:
"""
Creates a commit on a branch
Arguments:
stream_id {str} -- the stream you want to commit to
object_id {str} -- the hash of your commit object
branch_name {str}
-- the name of the branch to commit to (defaults to "main")
message {str}
-- optional: a message to give more information about the commit
source_application{str}
-- optional: the application from which the commit was created
(defaults to "python")
parents {List[str]} -- optional: the id of the parent commits
Returns:
str -- the id of the created commit
"""
query = gql(
"""
mutation CommitCreate ($commit: CommitCreateInput!)
{ commitCreate(commit: $commit)}
"""
)
params = {
"commit": {
"streamId": stream_id,
"branchName": branch_name,
"objectId": object_id,
"message": message,
"sourceApplication": source_application,
}
}
if parents:
params["commit"]["parents"] = parents
return self.make_request(
query=query, params=params, return_type="commitCreate", parse_response=False
)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
Update a commit
Arguments:
stream_id {str}
-- the id of the stream that contains the commit you'd like to update
commit_id {str} -- the id of the commit you'd like to update
message {str} -- the updated commit message
Returns:
bool -- True if the operation succeeded
"""
query = gql(
"""
mutation CommitUpdate($commit: CommitUpdateInput!)
{ commitUpdate(commit: $commit)}
"""
)
params = {
"commit": {"streamId": stream_id, "id": commit_id, "message": message}
}
return self.make_request(
query=query, params=params, return_type="commitUpdate", parse_response=False
)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
Delete a commit
Arguments:
stream_id {str}
-- the id of the stream that contains the commit you'd like to delete
commit_id {str} -- the id of the commit you'd like to delete
Returns:
bool -- True if the operation succeeded
"""
query = gql(
"""
mutation CommitDelete($commit: CommitDeleteInput!)
{ commitDelete(commit: $commit)}
"""
)
params = {"commit": {"streamId": stream_id, "id": commit_id}}
return self.make_request(
query=query, params=params, return_type="commitDelete", parse_response=False
)
def received(
self,
stream_id: str,
commit_id: str,
source_application: str = "python",
message: Optional[str] = None,
) -> bool:
"""
Mark a commit object a received by the source application.
"""
query = gql(
"""
mutation CommitReceive($receivedInput:CommitReceivedInput!){
commitReceive(input:$receivedInput)
}
"""
)
params = {
"receivedInput": {
"sourceApplication": source_application,
"streamId": stream_id,
"commitId": commit_id,
"message": "message",
}
}
try:
return self.make_request(
query=query,
params=params,
return_type="commitReceive",
parse_response=False,
)
except Exception as ex:
print(ex.with_traceback)
return False
@@ -0,0 +1,172 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from gql import gql
from specklepy.api.models import ActivityCollection, LimitedUser
from specklepy.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "other_user"
class Resource(ResourceBase):
"""API Access class for other users, that are not the currently active user."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = LimitedUser
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
Arguments:
id {str} -- the user id
Returns:
LimitedUser -- the retrieved profile of another user
"""
query = gql(
"""
query OtherUser($id: String!) {
otherUser(id: $id) {
id
name
bio
company
avatar
verified
role
}
}
"""
)
params = {"id": id}
return self.make_request(query=query, params=params, return_type="otherUser")
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""Searches for user by name or email. The search query must be at least
3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[LimitedUser] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
)
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
}
}
}
"""
)
params = {"search_query": search_query, "limit": limit}
return self.make_request(
query=query, params=params, return_type=["userSearch", "items"]
)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$user_id: String!,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
otherUser(id: $user_id) {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
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=["otherUser", "activity"],
schema=ActivityCollection,
)
+174
View File
@@ -0,0 +1,174 @@
import re
from typing import Any, Dict, List, Tuple
from gql import gql
from specklepy.api.models import ServerInfo
from specklepy.api.resource import ResourceBase
from specklepy.logging.exceptions import GraphQLException
NAME = "server"
class Resource(ResourceBase):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
)
def get(self) -> ServerInfo:
"""Get the server info
Returns:
dict -- the server info in dictionary form
"""
query = gql(
"""
query Server {
serverInfo {
name
company
description
adminContact
canonicalUrl
version
roles {
name
description
resourceTarget
}
scopes {
name
description
}
authStrategies{
id
name
icon
}
}
}
"""
)
return self.make_request(
query=query, return_type="serverInfo", schema=ServerInfo
)
def version(self) -> Tuple[Any, ...]:
"""Get the server version
Returns:
the server version in the format (major, minor, patch, (tag, build))
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
"""
# not tracking as it will be called along with other mutations / queries as a check
query = gql(
"""
query Server {
serverInfo {
version
}
}
"""
)
ver = self.make_request(
query=query, return_type=["serverInfo", "version"], parse_response=False
)
if isinstance(ver, Exception):
raise GraphQLException(
f"Could not get server version for {self.basepath}", [ver]
)
# pylint: disable=consider-using-generator; (list comp is faster)
return tuple(
[
int(segment) if segment.isdigit() else segment
for segment in re.split(r"\.|-", ver)
]
)
def apps(self) -> Dict:
"""Get the apps registered on the server
Returns:
dict -- a dictionary of apps registered on the server
"""
query = gql(
"""
query Apps {
apps{
id
name
description
termsAndConditionsLink
trustByDefault
logo
author {
id
name
avatar
}
}
}
"""
)
return self.make_request(query=query, return_type="apps", parse_response=False)
def create_token(self, name: str, scopes: List[str], lifespan: int) -> str:
"""Create a personal API token
Arguments:
scopes {List[str]} -- the scopes to grant with this token
name {str} -- a name for your new token
lifespan {int} -- duration before the token expires
Returns:
str -- the new API token. note: this is the only time you'll see the token!
"""
query = gql(
"""
mutation TokenCreate($token: ApiTokenCreateInput!) {
apiTokenCreate(token: $token)
}
"""
)
params = {"token": {"scopes": scopes, "name": name, "lifespan": lifespan}}
return self.make_request(
query=query,
params=params,
return_type="apiTokenCreate",
parse_response=False,
)
def revoke_token(self, token: str) -> bool:
"""Revokes (deletes) a personal API token
Arguments:
token {str} -- the token to revoke (delete)
Returns:
bool -- True if the token was successfully deleted
"""
query = gql(
"""
mutation TokenRevoke($token: String!) {
apiTokenRevoke(token: $token)
}
"""
)
params = {"token": token}
return self.make_request(
query=query,
params=params,
return_type="apiTokenRevoke",
parse_response=False,
)
+751
View File
@@ -0,0 +1,751 @@
from datetime import datetime, timezone
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, Stream
from specklepy.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
NAME = "stream"
class Resource(ResourceBase):
"""API Access class for streams"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = Stream
def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream:
"""Get the specified stream from the server
Arguments:
id {str} -- the stream id
branch_limit {int} -- the maximum number of branches to return
commit_limit {int} -- the maximum number of commits to return
Returns:
Stream -- the retrieved stream
"""
query = gql(
"""
query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) {
stream(id: $id) {
id
name
role
description
isPublic
createdAt
updatedAt
commentCount
favoritesCount
collaborators {
id
name
role
avatar
}
branches(limit: $branch_limit) {
totalCount
cursor
items {
id
name
description
commits(limit: $commit_limit) {
totalCount
cursor
items {
id
message
authorId
createdAt
authorName
referencedObject
sourceApplication
}
}
}
}
}
}
"""
)
params = {"id": id, "branch_limit": branch_limit, "commit_limit": commit_limit}
return self.make_request(query=query, params=params, return_type="stream")
def list(self, stream_limit: int = 10) -> List[Stream]:
"""Get a list of the user's streams
Arguments:
stream_limit {int} -- The maximum number of streams to return
Returns:
List[Stream] -- A list of Stream objects
"""
query = gql(
"""
query User($stream_limit: Int!) {
user {
id
bio
name
email
avatar
company
verified
profiles
role
streams(limit: $stream_limit) {
totalCount
cursor
items {
id
name
role
isPublic
createdAt
updatedAt
description
commentCount
favoritesCount
collaborators {
id
name
role
}
}
}
}
}
"""
)
params = {"stream_limit": stream_limit}
return self.make_request(
query=query, params=params, return_type=["user", "streams", "items"]
)
def create(
self,
name: str = "Anonymous Python Stream",
description: str = "No description provided",
is_public: bool = True,
) -> str:
"""Create a new stream
Arguments:
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
id {str} -- the id of the newly created stream
"""
query = gql(
"""
mutation StreamCreate($stream: StreamCreateInput!) {
streamCreate(stream: $stream)
}
"""
)
params = {
"stream": {"name": name, "description": description, "isPublic": is_public}
}
return self.make_request(
query=query, params=params, return_type="streamCreate", parse_response=False
)
def update(
self,
id: str,
name: Optional[str] = None,
description: Optional[str] = None,
is_public: Optional[bool] = None,
) -> bool:
"""Update an existing stream
Arguments:
id {str} -- the id of the stream to be updated
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
bool -- whether the stream update was successful
"""
query = gql(
"""
mutation StreamUpdate($stream: StreamUpdateInput!) {
streamUpdate(stream: $stream)
}
"""
)
params = {
"id": id,
"name": name,
"description": description,
"isPublic": is_public,
}
# remove None values so graphql doesn't cry
params = {"stream": {k: v for k, v in params.items() if v is not None}}
return self.make_request(
query=query, params=params, return_type="streamUpdate", parse_response=False
)
def delete(self, id: str) -> bool:
"""Delete a stream given its id
Arguments:
id {str} -- the id of the stream to delete
Returns:
bool -- whether the deletion was successful
"""
query = gql(
"""
mutation StreamDelete($id: String!) {
streamDelete(id: $id)
}
"""
)
params = {"id": id}
return self.make_request(
query=query, params=params, return_type="streamDelete", parse_response=False
)
def search(
self,
search_query: str,
limit: int = 25,
branch_limit: int = 10,
commit_limit: int = 10,
):
"""Search for streams by name, description, or id
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
branch_limit {int} -- the maximum number of branches to return
commit_limit {int} -- the maximum number of commits to return
Returns:
List[Stream] -- a list of Streams that match the search query
"""
query = gql(
"""
query StreamSearch(
$search_query: String!,
$limit: Int!,
$branch_limit:Int!,
$commit_limit:Int!
) {
streams(query: $search_query, limit: $limit) {
items {
id
name
role
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
}
}
}
}
}
}
}
"""
)
params = {
"search_query": search_query,
"limit": limit,
"branch_limit": branch_limit,
"commit_limit": commit_limit,
}
return self.make_request(
query=query, params=params, return_type=["streams", "items"]
)
def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream.
Arguments:
stream_id {str} -- the id of the stream to favorite / unfavorite
favorited {bool}
-- whether to favorite (True) or unfavorite (False) the stream
Returns:
Stream -- the stream with its `id`, `name`, and `favoritedDate`
"""
query = gql(
"""
mutation StreamFavorite($stream_id: String!, $favorited: Boolean!) {
streamFavorite(streamId: $stream_id, favorited: $favorited) {
id
name
favoritedDate
favoritesCount
}
}
"""
)
params = {
"stream_id": stream_id,
"favorited": favorited,
}
return self.make_request(
query=query, params=params, return_type=["streamFavorite"]
)
def get_all_pending_invites(
self, stream_id: str
) -> List[PendingStreamCollaborator]:
"""Get all of the pending invites on a stream.
You must be a `stream:owner` to query this.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the stream id from which to get the pending invites
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the specified stream
"""
self._check_invites_supported()
query = gql(
"""
query StreamInvites($streamId: String!) {
stream(id: $streamId){
pendingCollaborators {
id
token
inviteId
streamId
streamName
title
role
invitedBy{
id
name
company
avatar
}
user {
id
name
company
avatar
}
}
}
}
"""
)
params = {"streamId": stream_id}
return self.make_request(
query=query,
params=params,
return_type=["stream", "pendingCollaborators"],
schema=PendingStreamCollaborator,
)
def invite(
self,
stream_id: str,
email: Optional[str] = None,
user_id: Optional[str] = None,
role: str = "stream:contributor", # should default be reviewer?
message: Optional[str] = None,
):
"""Invite someone to a stream using either their email or user id
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
email {str} -- the email of the user to invite (use this OR `user_id`)
user_id {str} -- the id of the user to invite (use this OR `email`)
role {str}
-- the role to assign to the user (defaults to `stream:contributor`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
self._check_invites_supported()
if email is None and user_id is None:
raise SpeckleException(
"You must provide either an email or a user id to use the"
" `stream.invite` method"
)
query = gql(
"""
mutation StreamInviteCreate($input: StreamInviteCreateInput!) {
streamInviteCreate(input: $input)
}
"""
)
params = {
"email": email,
"userId": user_id,
"streamId": stream_id,
"message": message,
"role": role,
}
params = {"input": {k: v for k, v in params.items() if v is not None}}
return self.make_request(
query=query,
params=params,
return_type="streamInviteCreate",
parse_response=False,
)
def invite_batch(
self,
stream_id: str,
emails: Optional[List[str]] = None,
user_ids: Optional[List[None]] = None,
message: Optional[str] = None,
) -> bool:
"""Invite a batch of users to a specified stream.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
emails {List[str]}
-- the email of the user to invite (use this and/or `user_ids`)
user_id {List[str]}
-- the id of the user to invite (use this and/or `emails`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
self._check_invites_supported()
if emails is None and user_ids is None:
raise SpeckleException(
"You must provide either an email or a user id to use the"
" `stream.invite` method"
)
query = gql(
"""
mutation StreamInviteBatchCreate($input: [StreamInviteCreateInput!]!) {
streamInviteBatchCreate(input: $input)
}
"""
)
email_invites = [
{"streamId": stream_id, "message": message, "email": email}
for email in (emails if emails is not None else [])
if email is not None
]
user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id}
for user_id in (user_ids if user_ids is not None else [])
if user_id is not None
]
params = {"input": [*email_invites, *user_invites]}
return self.make_request(
query=query,
params=params,
return_type="streamInviteBatchCreate",
parse_response=False,
)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream invite
invite_id {str} -- the id of the invite to use
Returns:
bool -- true if the operation was successful
"""
self._check_invites_supported()
query = gql(
"""
mutation StreamInviteCancel($streamId: String!, $inviteId: String!) {
streamInviteCancel(streamId: $streamId, inviteId: $inviteId)
}
"""
)
params = {"streamId": stream_id, "inviteId": invite_id}
return self.make_request(
query=query,
params=params,
return_type="streamInviteCancel",
parse_response=False,
)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str}
-- the id of the stream for which the user has a pending invite
token {str} -- the token of the invite to use
accept {bool} -- whether or not to accept the invite (defaults to True)
Returns:
bool -- true if the operation was successful
"""
self._check_invites_supported()
query = gql(
"""
mutation StreamInviteUse(
$accept: Boolean!,
$streamId: String!,
$token: String!
) {
streamInviteUse(accept: $accept, streamId: $streamId, token: $token)
}
"""
)
params = {"streamId": stream_id, "token": token, "accept": accept}
return self.make_request(
query=query,
params=params,
return_type="streamInviteUse",
parse_response=False,
)
def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream
Valid for Speckle Server >=2.6.4
Arguments:
stream_id {str} -- the id of the stream to grant permissions to
user_id {str} -- the id of the user to grant permissions for
role {str} -- the role to grant the user
Returns:
bool -- True if the operation was successful
"""
if self.server_version and (
self.server_version != ("dev",) and self.server_version < (2, 6, 4)
):
raise UnsupportedException(
"Server mutation `update_permission` is only supported as of Speckle"
" Server v2.6.4. Please update your Speckle Server to use this method"
" or use the `grant_permission` method instead."
)
query = gql(
"""
mutation StreamUpdatePermission(
$permission_params: StreamUpdatePermissionInput!
) {
streamUpdatePermission(permissionParams: $permission_params)
}
"""
)
params = {
"permission_params": {
"streamId": stream_id,
"userId": user_id,
"role": role,
}
}
return self.make_request(
query=query,
params=params,
return_type="streamUpdatePermission",
parse_response=False,
)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
Arguments:
stream_id {str} -- the id of the stream to revoke permissions from
user_id {str} -- the id of the user to revoke permissions from
Returns:
bool -- True if the operation was successful
"""
query = gql(
"""
mutation StreamRevokePermission(
$permission_params: StreamRevokePermissionInput!
) {
streamRevokePermission(permissionParams: $permission_params)
}
"""
)
params = {"permission_params": {"streamId": stream_id, "userId": user_id}}
return self.make_request(
query=query,
params=params,
return_type="streamRevokePermission",
parse_response=False,
)
def activity(
self,
stream_id: str,
action_type: Optional[str] = None,
limit: int = 20,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
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,
)
@@ -106,6 +106,18 @@ def user_speckle_folder_path() -> Path:
return _ensure_folder_exists(user_application_data_path(), _application_name)
def user_speckle_connector_installation_path(host_application: str) -> Path:
"""
Gets a connector specific installation folder.
In this folder we can put our connector installation and all python packages.
"""
return _ensure_folder_exists(
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
host_application,
)
def accounts_folder_path() -> Path:
"""Get the folder where the Speckle accounts data should be stored."""
return _ensure_folder_exists(user_speckle_folder_path(), _accounts_folder_name)
+17 -14
View File
@@ -23,23 +23,24 @@ LOG = logging.getLogger(__name__)
METRICS_TRACKER = None
# actions
SDK = "SDK Actions"
RECEIVE = "Receive"
SEND = "Send"
STREAM = "Stream Action"
PERMISSION = "Permission Action"
INVITE = "Invite Action"
COMMIT = "Commit Action"
BRANCH = "Branch Action"
USER = "User Action"
OTHER_USER = "Other User Action"
SERVER = "Server Action"
CLIENT = "Speckle Client"
STREAM_WRAPPER = "Stream Wrapper"
# not in use since 2.15
ACCOUNTS = "Get Local Accounts"
SERIALIZE = "serialization/serialize"
BRANCH = "Branch Action"
CLIENT = "Speckle Client"
COMMIT = "Commit Action"
DESERIALIZE = "serialization/deserialize"
INVITE = "Invite Action"
OTHER_USER = "Other User Action"
PERMISSION = "Permission Action"
SERIALIZE = "serialization/serialize"
SERVER = "Server Action"
STREAM = "Stream Action"
STREAM_WRAPPER = "Stream Wrapper"
USER = "User Action"
def disable():
@@ -96,7 +97,7 @@ def initialise_tracker(account=None):
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)
METRICS_TRACKER.set_last_server(account.serverInfo.url)
class Singleton(type):
@@ -139,7 +140,9 @@ class MetricsTracker(metaclass=Singleton):
self.last_server = self.hash(server)
def hash(self, value: str):
return hashlib.md5(value.lower().encode("utf-8")).hexdigest().upper()
inputList = value.lower().split("://")
input = inputList[len(inputList)-1].split("/")[0].split('?')[0]
return hashlib.md5(input.encode("utf-8")).hexdigest().upper()
def _send_tracking_requests(self):
session = requests.Session()
+30
View File
@@ -0,0 +1,30 @@
from typing import Optional
from specklepy.objects import Base
class CRS(Base, speckle_type="Objects.GIS.CRS"):
"""A Coordinate Reference System stored in wkt format"""
def __init__(
self,
name: Optional[str] = None,
authority_id: Optional[str] = None,
wkt: Optional[str] = None,
units: Optional[str] = None,
units_native: Optional[str] = None,
offset_x: Optional[float] = None,
offset_y: Optional[float] = None,
rotation: Optional[float] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.name = name
self.authority_id = authority_id
self.wkt = wkt
self.units = units or "m"
self.units_native = units_native
self.offset_x = offset_x
self.offset_y = offset_y
self.rotation = rotation
+22
View File
@@ -0,0 +1,22 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.layers import (
VectorLayer,
RasterLayer,
)
from specklepy.objects.GIS.geometry import (
GisPolygonGeometry,
GisPolygonElement,
GisLineElement,
GisPointElement,
GisRasterElement,
)
from specklepy.objects.GIS.CRS import (
CRS,
)
__all__ = ["VectorLayer", "RasterLayer",
"GisPolygonGeometry", "GisPolygonElement", "GisLineElement", "GisPointElement", "GisRasterElement",
"CRS"]
+104
View File
@@ -0,0 +1,104 @@
from typing import Optional, Union, List
from specklepy.objects.geometry import Point, Line, Polyline, Circle, Arc, Polycurve, Mesh
from specklepy.objects import Base
from deprecated import deprecated
class GisPolygonGeometry(Base, speckle_type="Objects.GIS.PolygonGeometry", detachable={"displayValue"}):
"""GIS Polygon Geometry"""
def __init__(
self,
boundary: Optional[Union[Polyline, Arc, Line, Circle, Polycurve]] = None,
voids: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]] ] = None,
displayValue: Optional[List[Mesh]] = None,
units: Optional[str] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.boundary = boundary
self.voids = voids
self.displayValue = displayValue
self.units = units or "m"
class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"):
"""GIS Polygon element"""
def __init__(
self,
geometry: Optional[List[GisPolygonGeometry]] = None,
attributes: Optional[Base] = None,
units: Optional[str] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.geometry = geometry
self.attributes = attributes
self.units = units or "m"
class GisLineElement(Base, speckle_type="Objects.GIS.LineElement"):
"""GIS Polyline element"""
def __init__(
self,
geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None,
attributes: Optional[Base] = None,
units: Optional[str] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.geometry = geometry
self.attributes = attributes
self.units = units or "m"
class GisPointElement(Base, speckle_type="Objects.GIS.PointElement"):
"""GIS Point element"""
def __init__(
self,
geometry: Optional[List[Point]] = None,
attributes: Optional[Base] = None,
units: Optional[str] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.geometry = geometry
self.attributes = attributes
self.units = units or "m"
class GisRasterElement(Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}):
"""GIS Raster element"""
def __init__(
self,
band_count: Optional[int] = None,
band_names: Optional[List[str]] = None,
x_origin: Optional[float] = None,
y_origin: Optional[float] = None,
x_size: Optional[int] = None,
y_size: Optional[int] = None,
x_resolution: Optional[float] = None,
y_resolution: Optional[float] = None,
noDataValue: Optional[List[float]] = None,
displayValue: Optional[List[Mesh]] = None,
units: Optional[str] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.band_count = band_count
self.band_names = band_names
self.x_origin = x_origin
self.y_origin = y_origin
self.x_size = x_size
self.y_size = y_size
self.x_resolution = x_resolution
self.y_resolution = y_resolution
self.noDataValue = noDataValue
self.displayValue = displayValue
self.units = units or "m"
+96
View File
@@ -0,0 +1,96 @@
from typing import Any, Dict, List, Optional
from specklepy.objects.base import Base
from specklepy.objects.other import Collection
from specklepy.objects.GIS.CRS import CRS
from deprecated import deprecated
@deprecated(version="2.15", reason="Use VectorLayer or RasterLayer instead")
class Layer(Base, detachable={"features"}):
"""A GIS Layer"""
def __init__(
self,
name:str=None,
crs:CRS=None,
units: str = "m",
features: Optional[List[Base]] = None,
layerType: str = "None",
geomType: str = "None",
renderer: Optional[dict[str, Any]] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.name = name
self.crs = crs
self.units = units
self.type = layerType
self.features = features or []
self.geomType = geomType
self.renderer = renderer or {}
class VectorLayer(Collection, detachable={"elements"}, speckle_type="Objects.GIS.VectorLayer", serialize_ignore={"features"}):
"""GIS Vector Layer"""
def __init__(
self,
name: Optional[str]=None,
crs: Optional[CRS]=None,
units: Optional[str] = None,
elements: Optional[List[Base]] = None,
attributes: Optional[Base] = None,
geomType: Optional[str] = None,
renderer: Optional[Dict[str, Any]] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.name = name or ""
self.crs = crs
self.units = units
self.elements = elements or []
self.attributes = attributes
self.geomType = geomType or "None"
self.renderer = renderer or {}
self.collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class RasterLayer(Collection, detachable={"elements"}, speckle_type="Objects.GIS.RasterLayer", serialize_ignore={"features"}):
"""GIS Raster Layer"""
def __init__(
self,
name: Optional[str] = None,
crs: Optional[CRS]=None,
units: Optional[str] = None,
rasterCrs: Optional[CRS]=None,
elements: Optional[List[Base]] = None,
geomType: Optional[str] = None,
renderer: Optional[Dict[str, Any]] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.name = name or ""
self.crs = crs
self.units = units
self.rasterCrs = rasterCrs
self.elements = elements or []
self.geomType = geomType or "None"
self.renderer = renderer or {}
self.collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
+5 -1
View File
@@ -5,6 +5,7 @@ from typing import (
Any,
ClassVar,
Dict,
ForwardRef,
List,
Optional,
Set,
@@ -217,6 +218,9 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
return True, t(value)
if getattr(t, "__module__", None) == "typing":
if isinstance(t, ForwardRef):
return True, value
origin = getattr(t, "__origin__")
# below is what in nicer for >= py38
# origin = get_origin(t)
@@ -303,7 +307,7 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if isinstance(value, t):
return True, value
with contextlib.suppress(ValueError):
with contextlib.suppress(ValueError, TypeError):
if t is float and value is not None:
return True, float(value)
# TODO: dafuq, i had to add this not list check
+5 -1
View File
@@ -39,7 +39,11 @@ class Point(Base, speckle_type=GEOMETRY + "Point"):
return pt
class Pointcloud(Base, speckle_type=GEOMETRY + "Pointcloud"):
class Pointcloud(
Base,
speckle_type=GEOMETRY + "Pointcloud",
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
):
points: Optional[List[float]] = None
colors: Optional[List[int]] = None
sizes: Optional[List[float]] = None
+48 -7
View File
@@ -1,10 +1,12 @@
from typing import Any, List, Optional
from deprecated import deprecated
from specklepy.objects.geometry import Point, Vector
from .base import Base
OTHER = "Objects.Other."
OTHER_REVIT = OTHER + "Revit."
IDENTITY_TRANSFORM = [
1.0,
@@ -72,7 +74,7 @@ class DisplayStyle(Base, speckle_type=OTHER + "DisplayStyle"):
class Transform(
Base,
speckle_type=OTHER + "Transform",
serialize_ignore={"translation", "scaling", "is_identity"},
serialize_ignore={"translation", "scaling", "is_identity", "value"},
):
"""The 4x4 transformation matrix
@@ -84,12 +86,21 @@ class Transform(
_value: Optional[List[float]] = None
@property
@deprecated(version="2.12", reason="Use matrix")
def value(self) -> List[float]:
"""The transform matrix represented as a flat list of 16 floats"""
return self._value
@value.setter
def value(self, value: List[float]) -> None:
self.matrix = value
@property
def matrix(self) -> List[float]:
"""The transform matrix represented as a flat list of 16 floats"""
return self._value
@matrix.setter
def matrix(self, value: List[float]) -> None:
try:
value = [float(x) for x in value]
except (ValueError, TypeError) as error:
@@ -118,7 +129,7 @@ class Transform(
@property
def is_identity(self) -> bool:
return self.value == IDENTITY_TRANSFORM
return self._value == IDENTITY_TRANSFORM
def apply_to_point(self, point: Point) -> Point:
"""Transform a single speckle Point
@@ -236,15 +247,38 @@ class BlockDefinition(
geometry: Optional[List[Base]] = None
class BlockInstance(
Base, speckle_type=OTHER + "BlockInstance", detachable={"blockDefinition"}
class Instance(
Base, speckle_type=OTHER + "Instance", detachable={"definition"}
):
blockDefinition: Optional[BlockDefinition] = None
transform: Optional[Transform] = None
definition: Optional[Base] = None
class BlockInstance(
Instance, speckle_type=OTHER + "BlockInstance", serialize_ignore={"blockDefinition"}
):
@property
@deprecated(version="2.13", reason="Use definition")
def blockDefinition(self) -> Optional[BlockDefinition]:
if isinstance(self.definition, BlockDefinition):
return self.definition
return None
@blockDefinition.setter
def blockDefinition(self, value: Optional[BlockDefinition]) -> None:
self.definition = value
class RevitInstance(Instance, speckle_type=OTHER_REVIT + "RevitInstance"):
level: Optional[Base] = None
facingFlipped: bool
handFlipped: bool
parameters: Optional[Base] = None
elementId: Optional[str]
# TODO: prob move this into a built elements module, but just trialling this for now
class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter"):
class RevitParameter(
Base, speckle_type="Objects.BuiltElements.Revit.Parameter"
):
name: Optional[str] = None
value: Any = None
applicationUnitType: Optional[str] = None # eg UnitType UT_Length
@@ -255,3 +289,10 @@ class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter")
isShared: bool = False
isReadOnly: bool = False
isTypeParameter: bool = False
class Collection(
Base, speckle_type="Speckle.Core.Models.Collection", detachable={"elements"}
):
name: Optional[str] = None
collectionType: Optional[str] = None
elements: Optional[List[Base]] = None
+5 -1
View File
@@ -6,7 +6,10 @@ from specklepy.objects.structural.analysis import (
ModelSettings,
ModelUnits,
)
from specklepy.objects.structural.axis import Axis
from specklepy.objects.structural.axis import (
AxisType,
Axis
)
from specklepy.objects.structural.geometry import (
Element1D,
Element2D,
@@ -82,6 +85,7 @@ __all__ = [
"ElementType1D",
"ElementType2D",
"ElementType3D",
"AxisType",
"Axis",
"Node",
"Restraint",
+8 -1
View File
@@ -1,10 +1,17 @@
from typing import Optional
from enum import Enum
from specklepy.objects.base import Base
from specklepy.objects.geometry import Plane
class AxisType(int, Enum):
Cartesian = 0
Cylindrical = 1
Spherical = 2
class Axis(Base, speckle_type="Objects.Structural.Geometry.Axis"):
name: Optional[str] = None
axisType: Optional[str] = None
axisType: Optional[AxisType] = None
plane: Optional[Plane] = None
@@ -5,7 +5,7 @@ from specklepy.objects.base import Base
from specklepy.objects.structural.axis import Axis
from specklepy.objects.structural.materials import StructuralMaterial
STRUCTURAL_PROPERTY = "Objectives.Structural.Properties"
STRUCTURAL_PROPERTY = "Objects.Structural.Properties"
class MemberType(int, Enum):
@@ -65,6 +65,7 @@ class ShapeType(int, Enum):
Box = 7
Catalogue = 8
Explicit = 9
Undefined = 10
class PropertyTypeSpring(int, Enum):
@@ -90,7 +91,9 @@ class Property(Base, speckle_type=STRUCTURAL_PROPERTY):
name: Optional[str] = None
class SectionProfile(Base, speckle_type=STRUCTURAL_PROPERTY + ".SectionProfile"):
class SectionProfile(
Base, speckle_type=STRUCTURAL_PROPERTY + ".Profiles.SectionProfile"
):
name: Optional[str] = None
shapeType: Optional[ShapeType] = None
area: float = 0.0
@@ -18,6 +18,7 @@ class BatchSender(object):
stream_id,
token,
max_batch_size_mb=1,
max_batch_length=20000,
batch_buffer_length=10,
thread_count=4,
):
@@ -26,6 +27,7 @@ class BatchSender(object):
self._token = token
self.max_size = int(max_batch_size_mb * 1000 * 1000)
self.max_batch_length = int(max_batch_length)
self._batches = queue.Queue(batch_buffer_length)
self._crt_batch = []
self._crt_batch_size = 0
@@ -39,7 +41,11 @@ class BatchSender(object):
self._create_threads()
crt_obj_size = len(obj)
if not self._crt_batch or self._crt_batch_size + crt_obj_size < self.max_size:
crt_batch_length = len(self._crt_batch)
if not self._crt_batch or (
self._crt_batch_size + crt_obj_size < self.max_size
and crt_batch_length < self.max_batch_length
):
self._crt_batch.append((id, obj))
self._crt_batch_size += crt_obj_size
return
@@ -90,17 +96,18 @@ class BatchSender(object):
self._exception = self._exception or ex
LOG.error("ServerTransport sending thread error: " + str(ex))
def _bg_send_batch(self, session, batch):
def _bg_send_batch(self, session: requests.Session, batch):
object_ids = [obj[0] for obj in batch]
try:
server_has_object = session.post(
url=f"{self.server_url}/api/diff/{self.stream_id}",
data={"objects": json.dumps(object_ids)},
).json()
except Exception as ex:
response = session.post(
url=f"{self.server_url}/api/diff/{self.stream_id}",
data={"objects": json.dumps(object_ids)},
)
if response.status_code == 403:
raise SpeckleException(
f"Invalid credentials - cannot send objects to server {self.server_url}"
) from ex
)
response.raise_for_status()
server_has_object = response.json()
new_object_ids = [x for x in object_ids if not server_has_object[x]]
new_object_ids = set(new_object_ids)
+1 -1
View File
@@ -5,7 +5,7 @@ from warnings import warn
import requests
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.abstract_transport import AbstractTransport
+3 -1
View File
@@ -1,5 +1,6 @@
import random
import uuid
from urllib.parse import parse_qs, urlparse
import pytest
import requests
@@ -38,7 +39,8 @@ def seed_user(host):
)
if not r.ok:
raise Exception(f"Cannot seed user: {r.reason}")
access_code = r.text.split("access_code=")[1]
redirect_url = urlparse(r.headers.get("location"))
access_code = parse_qs(redirect_url.query)["access_code"][0] # type: ignore
r_tokens = requests.post(
url=f"http://{host}/auth/token",
+9
View File
@@ -44,3 +44,12 @@ def test_accounts_folder_name_override():
speckle_path_provider.override_accounts_folder_name(new_folder_name)
assert speckle_path_provider._accounts_folder_name == new_folder_name
speckle_path_provider.override_accounts_folder_name(old_folder_name)
def test_connector_installation_path():
host_application = "test application"
connector_path = speckle_path_provider.user_speckle_connector_installation_path(
host_application
)
assert "connector_installations" in str(connector_path)
assert str(connector_path).endswith(host_application)
+3
View File
@@ -106,6 +106,9 @@ fake_bases = [FakeBase("foo"), FakeBase("bar")]
True,
fake_bases,
),
(List["int"], [2, 3, 4], True, [2, 3, 4]),
(Union[float, Dict[str, float]], {"foo": 1, "bar": 2}, True, {"foo": 1.0, "bar": 2.0}),
(Union[float, Dict[str, float]], {"foo": "bar"}, False, {"foo": "bar"}),
],
)
def test_validate_type(
+207
View File
@@ -0,0 +1,207 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
_user_data_env_var = "SPECKLE_USERDATA_PATH"
def _path() -> Optional[Path]:
"""Read the user data path override setting."""
path_override = os.environ.get(_user_data_env_var)
if path_override:
return Path(path_override)
return None
_application_name = "Speckle"
def override_application_name(application_name: str) -> None:
"""Override the global Speckle application name."""
global _application_name
_application_name = application_name
def override_application_data_path(path: Optional[str]) -> None:
"""
Override the global Speckle application data path.
If the value of path is `None` the environment variable gets deleted.
"""
if path:
os.environ[_user_data_env_var] = path
else:
os.environ.pop(_user_data_env_var, None)
def _ensure_folder_exists(base_path: Path, folder_name: str) -> Path:
path = base_path.joinpath(folder_name)
path.mkdir(exist_ok=True, parents=True)
return path
def user_application_data_path() -> Path:
"""Get the platform specific user configuration folder path"""
path_override = _path()
if path_override:
return path_override
try:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception(
"Cannot get appdata path from environment."
)
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
# as that is used as an override
app_data_path = os.getenv("XDG_DATA_HOME")
if app_data_path:
return Path(app_data_path)
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception(
"Failed to initialize user application data path.", ex
)
def user_speckle_folder_path() -> Path:
"""Get the folder where the user's Speckle data should be stored."""
return _ensure_folder_exists(user_application_data_path(), _application_name)
def user_speckle_connector_installation_path(host_application: str) -> Path:
"""
Gets a connector specific installation folder.
In this folder we can put our connector installation and all python packages.
"""
return _ensure_folder_exists(
_ensure_folder_exists(user_speckle_folder_path(), "connector_installations"),
host_application,
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
if sys.path[0] != connector_installation_path:
sys.path.insert(0, str(connector_installation_path))
print(f"Using connector installation path {connector_installation_path}")
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
return True
except ImportError:
return False
def ensure_pip() -> None:
print("Installing pip... ")
from subprocess import run
completed_process = run([PYTHON_PATH, "-m", "ensurepip"])
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
def get_requirements_path() -> Path:
# we assume that a requirements.txt exists next to the __init__.py file
path = Path(Path(__file__).parent, "requirements.txt")
assert path.exists()
return path
def install_requirements(host_application: str) -> None:
# set up addons/modules under the user
# script path. Here we'll install the
# dependencies
path = connector_installation_path(host_application)
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
completed_process = run(
[
PYTHON_PATH,
"-m",
"pip",
"install",
"-t",
str(path),
"-r",
str(get_requirements_path()),
],
capture_output=True,
text=True,
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
def install_dependencies(host_application: str) -> None:
if not is_pip_available():
ensure_pip()
install_requirements(host_application)
def _import_dependencies() -> None:
import_module("specklepy")
# the code above doesn't work for now, it fails on importing graphql-core
# despite that, the connector seams to be working as expected
# But it would be nice to make this solution work
# it would ensure that all dependencies are fully loaded
# requirements = get_requirements_path().read_text()
# reqs = [
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
# for req in requirements.split("\n")
# if req and not req.startswith(" ")
# ]
# for req in reqs:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
invalidate_caches()
_import_dependencies()
print("Successfully found dependencies")
except ImportError:
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")