Compare commits

..

3 Commits

Author SHA1 Message Date
Gergő Jedlicska 0330c6cf83 style(installer): fix line length 2023-05-17 18:50:45 +02:00
Gergő Jedlicska e98a80d45a style(installer): fix style 2023-05-17 18:48:34 +02:00
Gergő Jedlicska 2d7e0e9e92 feat(universal-installer): use pkg_resources to check for dependecies installed 2023-05-17 18:47:54 +02:00
36 changed files with 1508 additions and 2749 deletions
+33 -23
View File
@@ -6,38 +6,48 @@ orbs:
jobs:
test:
machine:
image: ubuntu-2204:2023.02.1
docker_layer_caching: true
resource_class: medium
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"
parameters:
tag:
default: "3.11"
default: "3.8"
type: string
steps:
- checkout
- run: python --version
- run:
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
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
- store_test_results:
path: reports
- store_artifacts:
path: reports
- codecov/upload
deploy:
@@ -55,7 +65,7 @@ workflows:
- test:
matrix:
parameters:
tag: ["3.11"]
tag: ["3.7", "3.8", "3.9", "3.10", "3.11"]
filters:
tags:
only: /.*/
+12
View File
@@ -0,0 +1,12 @@
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
@@ -0,0 +1,12 @@
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 }}
-162
View File
@@ -1,162 +0,0 @@
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:
+5 -1
View File
@@ -9,7 +9,7 @@ from gql.transport.requests import RequestsHTTPTransport
from gql.transport.websockets import WebsocketsTransport
from specklepy.api import resources
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.api.resources import (
active_user,
branch,
@@ -60,6 +60,7 @@ class SpeckleClient:
USE_SSL = True
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
metrics.track(metrics.CLIENT, custom_props={"name": "create"})
ws_protocol = "ws"
http_protocol = "http"
@@ -128,6 +129,7 @@ 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:
@@ -139,10 +141,12 @@ 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",
+108 -10
View File
@@ -9,11 +9,37 @@ from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
# following imports seem to be unnecessary, but they need to stay
# 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)
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
@@ -25,19 +51,53 @@ 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 = core_get_local_accounts(base_path)
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
metrics.track(
metrics.SDK,
metrics.ACCOUNTS,
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,
@@ -48,7 +108,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 = core_get_local_accounts(base_path=base_path)
accounts = get_local_accounts(base_path=base_path)
if not accounts:
return None
@@ -59,4 +119,42 @@ 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(),
)
+61 -12
View File
@@ -7,11 +7,6 @@ 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,
@@ -29,13 +24,31 @@ def send(
Returns:
str -- the object id of the sent object
"""
obj_hash = core_send(base, transports, use_default_cache)
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:
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
@@ -43,6 +56,15 @@ 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.
@@ -55,8 +77,29 @@ def receive(
Returns:
Base -- the base object
"""
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
return _untracked_receive(obj_id, remote_transport, local_transport)
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:
@@ -73,9 +116,11 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
Returns:
str -- the serialized object
"""
metrics.track(metrics.SDK, custom_props={"name": "Serialize"})
metrics.track(metrics.SERIALIZE)
serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1]
return core_serialize(base, write_transports)
def deserialize(
obj_string: str, read_transport: Optional[AbstractTransport] = None
@@ -96,9 +141,13 @@ def deserialize(
Returns:
Base -- the deserialized object
"""
metrics.track(metrics.SDK, custom_props={"name": "Deserialize"})
metrics.track(metrics.DESERIALIZE)
if not read_transport:
read_transport = SQLiteTransport()
return core_deserialize(obj_string, read_transport)
serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string)
__all__ = ["receive", "send", "serialize", "deserialize"]
+1 -1
View File
@@ -4,7 +4,7 @@ from gql.client import Client
from gql.transport.exceptions import TransportQueryError
from graphql import DocumentNode
from specklepy.core.api.credentials import Account
from specklepy.api.credentials import Account
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
+189 -14
View File
@@ -8,10 +8,10 @@ from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.active_user import NAME, Resource as Core_Resource
NAME = "active_user"
class Resource(Core_Resource):
class Resource(ResourceBase):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
@@ -19,6 +19,7 @@ class Resource(Core_Resource):
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = User
@@ -34,9 +35,28 @@ class Resource(Core_Resource):
Returns:
User -- the retrieved user
"""
metrics.track(metrics.SDK, custom_props={"name": "User Get"})
return super().get()
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")
def update(
self,
@@ -56,9 +76,106 @@ class Resource(Core_Resource):
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.SDK, self.account, {"name": "User Update"})
return super().update(name, company, bio, avatar)
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,
)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
@@ -69,9 +186,36 @@ class Resource(Core_Resource):
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
metrics.track(metrics.SDK, self.account, {"name": "Invite Get"})
return super().get_all_pending_invites()
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,
)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
@@ -89,6 +233,37 @@ class Resource(Core_Resource):
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.SDK, self.account, {"name": "Invite Get"})
return super().get_pending_invite(stream_id, token)
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,
)
+138 -16
View File
@@ -6,10 +6,10 @@ from specklepy.api.models import Branch
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.core.api.resources.branch import NAME, Resource as Core_Resource
NAME = "branch"
class Resource(Core_Resource):
class Resource(ResourceBase):
"""API Access class for branches"""
def __init__(self, account, basepath, client) -> None:
@@ -17,6 +17,7 @@ class Resource(Core_Resource):
account=account,
basepath=basepath,
client=client,
name=NAME,
)
self.schema = Branch
@@ -32,9 +33,25 @@ class Resource(Core_Resource):
Returns:
id {str} -- the newly created branch's id
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
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,
}
}
return super().create(stream_id, name, 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
@@ -47,9 +64,42 @@ class Resource(Core_Resource):
Returns:
Branch -- the fetched branch with its latest commits
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Get"})
return super().get(stream_id, name, commits_limit)
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"]
)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
@@ -62,9 +112,50 @@ class Resource(Core_Resource):
Returns:
List[Branch] -- the branches on the stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Get"})
return super().list(stream_id, branches_limit, commits_limit)
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"]
)
def update(
self,
@@ -84,9 +175,29 @@ class Resource(Core_Resource):
Returns:
bool -- True if update is successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Update"})
return super().update(stream_id, branch_id, name, description)
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
)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
@@ -98,6 +209,17 @@ class Resource(Core_Resource):
Returns:
bool -- True if deletion is successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Delete"})
return super().delete(stream_id, branch_id)
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
)
+134 -20
View File
@@ -6,10 +6,10 @@ from specklepy.api.models import Commit
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.core.api.resources.commit import NAME, Resource as Core_Resource
NAME = "commit"
class Resource(Core_Resource):
class Resource(ResourceBase):
"""API Access class for commits"""
def __init__(self, account, basepath, client) -> None:
@@ -17,6 +17,7 @@ class Resource(Core_Resource):
account=account,
basepath=basepath,
client=client,
name=NAME,
)
self.schema = Commit
@@ -31,9 +32,32 @@ class Resource(Core_Resource):
Returns:
Commit -- the retrieved commit object
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Get"})
return super().get(stream_id, commit_id)
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]:
"""
@@ -46,9 +70,36 @@ class Resource(Core_Resource):
Returns:
List[Commit] -- a list of the most recent commit objects
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Get"})
return super().list(stream_id, limit)
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"]
)
def create(
self,
@@ -77,9 +128,28 @@ class Resource(Core_Resource):
Returns:
str -- the id of the created commit
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Create"})
return super().create(stream_id, object_id, branch_name, message, source_application, parents)
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
)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
@@ -94,9 +164,20 @@ class Resource(Core_Resource):
Returns:
bool -- True if the operation succeeded
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Update"})
return super().update(stream_id, commit_id, message)
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
)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
@@ -110,9 +191,18 @@ class Resource(Core_Resource):
Returns:
bool -- True if the operation succeeded
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Delete"})
return super().delete(stream_id, commit_id)
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
)
def received(
self,
@@ -124,6 +214,30 @@ class Resource(Core_Resource):
"""
Mark a commit object a received by the source application.
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Received"})
return super().received(stream_id, commit_id, source_application, message)
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
+123 -8
View File
@@ -8,10 +8,10 @@ from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.other_user import NAME, Resource as Core_Resource
NAME = "other_user"
class Resource(Core_Resource):
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:
@@ -19,6 +19,7 @@ class Resource(Core_Resource):
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = LimitedUser
@@ -33,9 +34,26 @@ class Resource(Core_Resource):
Returns:
LimitedUser -- the retrieved profile of another user
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
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")
def search(
self, search_query: str, limit: int = 25
@@ -54,7 +72,104 @@ class Resource(Core_Resource):
message="User search query must be at least 3 characters"
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
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}
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,
)
+119 -10
View File
@@ -8,10 +8,10 @@ from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import GraphQLException
from specklepy.core.api.resources.server import NAME, Resource as Core_Resource
NAME = "server"
class Resource(Core_Resource):
class Resource(ResourceBase):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
@@ -19,6 +19,7 @@ class Resource(Core_Resource):
account=account,
basepath=basepath,
client=client,
name=NAME,
)
def get(self) -> ServerInfo:
@@ -27,9 +28,72 @@ class Resource(Core_Resource):
Returns:
dict -- the server info in dictionary form
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Get"})
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
}
}
}
"""
)
return super().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)
]
)
def apps(self) -> Dict:
"""Get the apps registered on the server
@@ -37,9 +101,28 @@ class Resource(Core_Resource):
Returns:
dict -- a dictionary of apps registered on the server
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Apps"})
metrics.track(metrics.SERVER, self.account, {"name": "apps"})
query = gql(
"""
query Apps {
apps{
id
name
description
termsAndConditionsLink
trustByDefault
logo
author {
id
name
avatar
}
}
}
"""
)
return super().apps()
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
@@ -52,9 +135,22 @@ class Resource(Core_Resource):
Returns:
str -- the new API token. note: this is the only time you'll see the token!
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Create 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}}
return super().create_token(name, scopes, 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
@@ -65,6 +161,19 @@ class Resource(Core_Resource):
Returns:
bool -- True if the token was successfully deleted
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Revoke Token"})
metrics.track(metrics.SERVER, self.account, {"name": "revoke_token"})
query = gql(
"""
mutation TokenRevoke($token: String!) {
apiTokenRevoke(token: $token)
}
"""
)
params = {"token": token}
return super().revoke_token(token)
return self.make_request(
query=query,
params=params,
return_type="apiTokenRevoke",
parse_response=False,
)
+511 -31
View File
@@ -9,10 +9,10 @@ from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
from specklepy.core.api.resources.stream import NAME, Resource as Core_Resource
NAME = "stream"
class Resource(Core_Resource):
class Resource(ResourceBase):
"""API Access class for streams"""
def __init__(self, account, basepath, client, server_version) -> None:
@@ -20,6 +20,7 @@ class Resource(Core_Resource):
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
@@ -36,9 +37,56 @@ class Resource(Core_Resource):
Returns:
Stream -- the retrieved stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Get"})
metrics.track(metrics.STREAM, self.account, {"name": "get"})
query = gql(
"""
query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) {
stream(id: $id) {
id
name
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
}
}
}
}
}
}
"""
)
return super().get(id, branch_limit, commit_limit)
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
@@ -49,9 +97,50 @@ class Resource(Core_Resource):
Returns:
List[Stream] -- A list of Stream objects
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Get"})
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
}
}
}
}
}
"""
)
return super().list(stream_limit)
params = {"stream_limit": stream_limit}
return self.make_request(
query=query, params=params, return_type=["user", "streams", "items"]
)
def create(
self,
@@ -70,9 +159,22 @@ class Resource(Core_Resource):
Returns:
id {str} -- the id of the newly created stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Create"})
metrics.track(metrics.STREAM, self.account, {"name": "create"})
query = gql(
"""
mutation StreamCreate($stream: StreamCreateInput!) {
streamCreate(stream: $stream)
}
"""
)
return super().create(name, description, is_public)
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,
@@ -93,9 +195,27 @@ class Resource(Core_Resource):
Returns:
bool -- whether the stream update was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Update"})
metrics.track(metrics.STREAM, self.account, {"name": "update"})
query = gql(
"""
mutation StreamUpdate($stream: StreamUpdateInput!) {
streamUpdate(stream: $stream)
}
"""
)
return super().update(id, name, description, is_public)
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
@@ -106,9 +226,20 @@ class Resource(Core_Resource):
Returns:
bool -- whether the deletion was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Delete"})
metrics.track(metrics.STREAM, self.account, {"name": "delete"})
query = gql(
"""
mutation StreamDelete($id: String!) {
streamDelete(id: $id)
}
"""
)
return super().delete(id)
params = {"id": id}
return self.make_request(
query=query, params=params, return_type="streamDelete", parse_response=False
)
def search(
self,
@@ -128,9 +259,67 @@ class Resource(Core_Resource):
Returns:
List[Stream] -- a list of Streams that match the search query
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Search"})
metrics.track(metrics.STREAM, self.account, {"name": "search"})
query = gql(
"""
query StreamSearch(
$search_query: String!,
$limit: Int!,
$branch_limit:Int!,
$commit_limit:Int!
) {
streams(query: $search_query, limit: $limit) {
items {
id
name
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
}
}
}
}
}
}
}
"""
)
return super().search(search_query, limit, branch_limit, commit_limit)
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.
@@ -143,9 +332,28 @@ class Resource(Core_Resource):
Returns:
Stream -- the stream with its `id`, `name`, and `favoritedDate`
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Favorite"})
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
}
}
"""
)
return super().favorite(stream_id, favorited)
params = {
"stream_id": stream_id,
"favorited": favorited,
}
return self.make_request(
query=query, params=params, return_type=["streamFavorite"]
)
@deprecated(
version="2.6.4",
@@ -167,7 +375,7 @@ class Resource(Core_Resource):
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 (
@@ -220,9 +428,46 @@ class Resource(Core_Resource):
List[PendingStreamCollaborator]
-- a list of pending invites for the specified stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Invite Get"})
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported()
return super().get_all_pending_invites(stream_id)
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,
@@ -248,9 +493,38 @@ class Resource(Core_Resource):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Invite Create"})
metrics.track(metrics.INVITE, self.account, {"name": "create"})
self._check_invites_supported()
return super().invite(stream_id, email, user_id, role, message)
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,
@@ -275,9 +549,43 @@ class Resource(Core_Resource):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Invite Batch Create"})
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"
)
return super().invite_batch(stream_id, emails, user_ids, message)
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
@@ -291,9 +599,25 @@ class Resource(Core_Resource):
Returns:
bool -- true if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Invite Cancel"})
metrics.track(metrics.INVITE, self.account, {"name": "cancel"})
self._check_invites_supported()
return super().invite_cancel(stream_id, invite_id)
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
@@ -309,9 +633,29 @@ class Resource(Core_Resource):
Returns:
bool -- true if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Invite Use"})
metrics.track(metrics.INVITE, self.account, {"name": "use"})
self._check_invites_supported()
return super().invite_use(stream_id, token, accept)
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
@@ -326,9 +670,41 @@ class Resource(Core_Resource):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Permission Update", "role": role})
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)
}
"""
)
return super().update_permission(stream_id, user_id, 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,
)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
@@ -340,7 +716,111 @@ class Resource(Core_Resource):
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Permission Revoke"})
metrics.track(metrics.PERMISSION, self.account, {"name": "revoke"})
query = gql(
"""
mutation StreamRevokePermission(
$permission_params: StreamRevokePermissionInput!
) {
streamRevokePermission(permissionParams: $permission_params)
}
"""
)
return super().revoke_permission(stream_id, user_id)
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,
)
+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(
+3 -1
View File
@@ -2,11 +2,12 @@ from urllib.parse import unquote, urlparse
from warnings import warn
from specklepy.api.client import SpeckleClient
from specklepy.core.api.credentials import (
from specklepy.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
@@ -74,6 +75,7 @@ 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
@@ -1,152 +0,0 @@
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
@@ -1,139 +0,0 @@
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"]
@@ -1,264 +0,0 @@
from datetime import datetime, timezone
from typing import List, Optional
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.logging.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
@@ -1,219 +0,0 @@
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
@@ -1,237 +0,0 @@
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
@@ -1,172 +0,0 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from gql import gql
from specklepy.api.models import ActivityCollection, LimitedUser
from specklepy.api.resource import ResourceBase
from specklepy.logging.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
@@ -1,174 +0,0 @@
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
@@ -1,751 +0,0 @@
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,
)
+15 -18
View File
@@ -23,24 +23,23 @@ LOG = logging.getLogger(__name__)
METRICS_TRACKER = None
# actions
SDK = "SDK Actions"
RECEIVE = "Receive"
SEND = "Send"
# not in use since 2.15
ACCOUNTS = "Get Local Accounts"
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"
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"
ACCOUNTS = "Get Local Accounts"
SERIALIZE = "serialization/serialize"
DESERIALIZE = "serialization/deserialize"
def disable():
@@ -97,7 +96,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.serverInfo.url)
METRICS_TRACKER.set_last_server(account.userInfo.email)
class Singleton(type):
@@ -140,9 +139,7 @@ class MetricsTracker(metaclass=Singleton):
self.last_server = self.hash(server)
def hash(self, value: str):
inputList = value.lower().split("://")
input = inputList[len(inputList)-1].split("/")[0].split('?')[0]
return hashlib.md5(input.encode("utf-8")).hexdigest().upper()
return hashlib.md5(value.lower().encode("utf-8")).hexdigest().upper()
def _send_tracking_requests(self):
session = requests.Session()
-30
View File
@@ -1,30 +0,0 @@
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
@@ -1,22 +0,0 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.layers import (
VectorLayer,
RasterLayer,
)
from specklepy.objects.GIS.geometry import (
GisPolygonGeometry,
GisPolygonElement,
GisLineElement,
GisPointElement,
GisRasterElement,
)
from specklepy.objects.GIS.CRS import (
CRS,
)
__all__ = ["VectorLayer", "RasterLayer",
"GisPolygonGeometry", "GisPolygonElement", "GisLineElement", "GisPointElement", "GisRasterElement",
"CRS"]
-104
View File
@@ -1,104 +0,0 @@
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
@@ -1,96 +0,0 @@
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
+1 -5
View File
@@ -39,11 +39,7 @@ class Point(Base, speckle_type=GEOMETRY + "Point"):
return pt
class Pointcloud(
Base,
speckle_type=GEOMETRY + "Pointcloud",
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
):
class Pointcloud(Base, speckle_type=GEOMETRY + "Pointcloud"):
points: Optional[List[float]] = None
colors: Optional[List[int]] = None
sizes: Optional[List[float]] = None
@@ -65,7 +65,6 @@ class ShapeType(int, Enum):
Box = 7
Catalogue = 8
Explicit = 9
Undefined = 10
class PropertyTypeSpring(int, Enum):
@@ -91,9 +90,7 @@ class Property(Base, speckle_type=STRUCTURAL_PROPERTY):
name: Optional[str] = None
class SectionProfile(
Base, speckle_type=STRUCTURAL_PROPERTY + ".Profiles.SectionProfile"
):
class SectionProfile(Base, speckle_type=STRUCTURAL_PROPERTY + ".Profiles.SectionProfile"):
name: Optional[str] = None
shapeType: Optional[ShapeType] = None
area: float = 0.0
@@ -18,7 +18,6 @@ class BatchSender(object):
stream_id,
token,
max_batch_size_mb=1,
max_batch_length=20000,
batch_buffer_length=10,
thread_count=4,
):
@@ -27,7 +26,6 @@ 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
@@ -41,11 +39,7 @@ class BatchSender(object):
self._create_threads()
crt_obj_size = len(obj)
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
):
if not self._crt_batch or self._crt_batch_size + crt_obj_size < self.max_size:
self._crt_batch.append((id, obj))
self._crt_batch_size += crt_obj_size
return
+1 -1
View File
@@ -5,7 +5,7 @@ from warnings import warn
import requests
from specklepy.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.abstract_transport import AbstractTransport
+35 -40
View File
@@ -3,9 +3,11 @@ Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from importlib import import_module, invalidate_caches
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
import pkg_resources
_user_data_env_var = "SPECKLE_USERDATA_PATH"
@@ -55,9 +57,7 @@ def user_application_data_path() -> Path:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception(
"Cannot get appdata path from environment."
)
raise Exception("Cannot get appdata path from environment.")
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
@@ -68,9 +68,7 @@ def user_application_data_path() -> Path:
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception(
"Failed to initialize user application data path.", ex
)
raise Exception("Failed to initialize user application data path.", ex)
def user_speckle_folder_path() -> Path:
@@ -90,19 +88,16 @@ def user_speckle_connector_installation_path(host_application: str) -> Path:
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path = user_speckle_connector_installation_path(
host_application
)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
@@ -113,7 +108,6 @@ def connector_installation_path(host_application: str) -> Path:
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
@@ -132,13 +126,15 @@ def ensure_pip() -> None:
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
raise Exception(
f"Failed to install pip, got {completed_process.returncode} return code"
)
def get_requirements_path() -> Path:
# we assume that a requirements.txt exists next to the __init__.py file
path = Path(Path(__file__).parent, "requirements.txt")
assert path.exists()
path = Path(__file__).parent.with_name("requirements.txt")
assert path.exists(), f"Can't find requirements file at {path}"
return path
@@ -167,7 +163,10 @@ def install_requirements(host_application: str) -> None:
)
if completed_process.returncode != 0:
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
m = (
"Failed to install dependenices through pip, ",
f"got {completed_process.returncode} return code",
)
print(m)
raise Exception(m)
@@ -179,29 +178,25 @@ def install_dependencies(host_application: str) -> None:
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 _dependencies_installed() -> bool:
try:
pkg_resources.require(get_requirements_path().read_text())
return True
except (pkg_resources.DistributionNotFound, pkg_resources.VersionConflict):
return False
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
invalidate_caches()
_import_dependencies()
if _dependencies_installed():
return
install_dependencies(host_application)
invalidate_caches()
if _dependencies_installed():
print("Successfully found dependencies")
except ImportError:
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
return
raise Exception(
"Cannot automatically ensure Speckle dependencies. ",
f"Please try restarting the host application {host_application}!",
)