Compare commits

..

1 Commits

Author SHA1 Message Date
Gergő Jedlicska 13e0b65c31 make test fail louder 2023-09-07 14:43:49 +02:00
121 changed files with 1994 additions and 7302 deletions
+3 -41
View File
@@ -1,47 +1,14 @@
version: 2.1
orbs:
codecov: codecov/codecov@3.3.0
python: circleci/python@2.0.3
codecov: codecov/codecov@3.2.2
jobs:
pre-commit:
parameters:
config_file:
default: ./.pre-commit-config.yaml
description: Optional, path to pre-commit config file.
type: string
cache_prefix:
default: ''
description: |
Optional cache prefix to be used on CircleCI. Can be used for cache busting or to ensure multiple jobs use different caches.
type: string
docker:
- image: speckle/pre-commit-runner:latest
resource_class: medium
steps:
- checkout
- restore_cache:
keys:
- cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
- run:
name: Install pre-commit hooks
command: pre-commit install-hooks --config <<parameters.config_file>>
- save_cache:
key: cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
paths:
- ~/.cache/pre-commit
- run:
name: Run pre-commit
command: pre-commit run --all-files
- run:
command: git --no-pager diff
name: git diff
when: on_fail
test:
machine:
image: ubuntu-2204:2023.02.1
docker_layer_caching: false
docker_layer_caching: true
resource_class: medium
parameters:
tag:
@@ -85,10 +52,6 @@ jobs:
workflows:
main:
jobs:
- pre-commit:
filters:
tags:
only: /.*/
- test:
matrix:
parameters:
@@ -99,7 +62,6 @@ workflows:
- deploy:
context: pypi
requires:
- pre-commit
- test
filters:
tags:
+1 -1
View File
@@ -22,6 +22,6 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
USER vscode
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
ENV PATH=$PATH:$HOME/.poetry/env
+9 -9
View File
@@ -2,23 +2,23 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
hooks:
- id: ruff
rev: v0.1.6
rev: v0.0.186
- repo: https://github.com/commitizen-tools/commitizen
hooks:
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: v3.13.0
- id: commitizen
- id: commitizen-branch
stages:
- push
rev: v2.38.0
- repo: https://github.com/pycqa/isort
rev: 5.12.0
rev: v5.11.3
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.11.0
rev: 22.12.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
@@ -27,7 +27,7 @@ repos:
# https://pre-commit.com/#top_level-default_language_version
# language_version: python3.11
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.4.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+36 -6
View File
@@ -2,16 +2,46 @@
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
Speckle | specklepy 🐍
</h1>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
<h3 align="center">
The Python SDK
</h3>
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://github.com/specklesystems/specklepy/"><img src="https://circleci.com/gh/specklesystems/specklepy.svg?style=svg&amp;circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a><a href="https://codecov.io/gh/specklesystems/specklepy">
<img src="https://codecov.io/gh/specklesystems/specklepy/branch/main/graph/badge.svg?token=8KQFL5N0YF"/>
</a> </p>
# About Speckle
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
### Features
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
- **Collaboration:** share your designs collaborate with others
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
- **Real time:** get real time updates and notifications and changes
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
<p align="center"><a href="https://codecov.io/gh/specklesystems/specklepy"><img src="https://codecov.io/gh/specklesystems/specklepy/branch/main/graph/badge.svg?token=8KQFL5N0YF" alt="Codecov"></a></p>
# Repo structure
+54 -12
View File
@@ -33,7 +33,7 @@ services:
retries: 30
minio:
image: "minio/minio:RELEASE.2023-10-25T06-33-25Z"
image: "minio/minio"
command: server /data --console-address ":9001"
restart: always
volumes:
@@ -52,26 +52,28 @@ services:
####
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend-2:latest
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:latest
image: speckle/speckle-server:2
restart: always
healthcheck:
test:
- CMD
- /nodejs/bin/node
- -e
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(res.statusCode != 200 || body.toLowerCase().includes('error'));}); }).end(); } catch { process.exit(1); }"
[
"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: 10s
retries: 3
start_period: 90s
timeout: 3s
retries: 30
ports:
- "0.0.0.0:3000:3000"
depends_on:
@@ -96,7 +98,6 @@ services:
S3_CREATE_BUCKET: "true"
FILE_SIZE_LIMIT_MB: 100
MAX_PROJECT_MODELS_PER_PAGE: 500
# TODO: Change this to a unique secret for this server
SESSION_SECRET: "TODO:ReplaceWithLongString"
@@ -110,6 +111,47 @@ services:
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
Generated
+1012 -1218
View File
File diff suppressed because it is too large Load Diff
+6 -10
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "specklepy"
version = "2.17.14"
version = "2.9.1"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -10,35 +10,31 @@ documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
packages = [
{ include = "specklepy", from = "src" },
{ include = "speckle_automate", from = "src" },
]
[tool.poetry.dependencies]
python = ">=3.8.0, <4.0"
pydantic = "^2.5"
python = ">=3.7.2, <4.0"
pydantic = "^2.0"
appdirs = "^1.4.4"
gql = { extras = ["requests", "websockets"], version = "^3.3.0" }
gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
ujson = "^5.3.0"
Deprecated = "^1.2.13"
stringcase = "^1.2.0"
attrs = "^23.1.0"
httpx = "^0.25.0"
[tool.poetry.group.dev.dependencies]
black = "23.11.0"
black = "^22.8.0"
isort = "^5.7.0"
pytest = "^7.1.3"
pytest-asyncio = "^0.23.0"
pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
devtools = "^0.8.0"
pylint = "^2.14.4"
pydantic-settings = "^2.3.0"
mypy = "^0.982"
pre-commit = "^2.20.0"
commitizen = "^2.38.0"
ruff = "^0.4.4"
ruff = "^0.0.187"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
-24
View File
@@ -1,24 +0,0 @@
"""This module contains an SDK for working with Speckle Automate."""
from speckle_automate.automation_context import AutomationContext
from speckle_automate.runner import execute_automate_function, run_function
from speckle_automate.schema import (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResultLevel,
ResultCase,
)
__all__ = [
"AutomationContext",
"AutomateBase",
"AutomationStatus",
"AutomationResult",
"AutomationRunData",
"ResultCase",
"ObjectResultLevel",
"run_function",
"execute_automate_function",
]
-424
View File
@@ -1,424 +0,0 @@
"""This module provides an abstraction layer above the Speckle Automate runtime."""
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
import httpx
from gql import gql
from speckle_automate.schema import (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResultLevel,
ResultCase,
)
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api.models import Branch
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.transports.memory import MemoryTransport
from specklepy.transports.server import ServerTransport
@dataclass
class AutomationContext:
"""A context helper class.
This class exposes methods to work with the Speckle Automate context inside
Speckle Automate functions.
An instance of AutomationContext is injected into every run of a function.
"""
automation_run_data: AutomationRunData
speckle_client: SpeckleClient
_server_transport: ServerTransport
_speckle_token: str
#: keep a memory transponrt at hand, to speed up things if needed
_memory_transport: MemoryTransport = field(default_factory=MemoryTransport)
#: added for performance measuring
_init_time: float = field(default_factory=time.perf_counter)
_automation_result: AutomationResult = field(default_factory=AutomationResult)
@classmethod
def initialize(
cls, automation_run_data: Union[str, AutomationRunData], speckle_token: str
) -> "AutomationContext":
"""Bootstrap the AutomateSDK from raw data.
Todo:
----
* bootstrap a structlog logger instance
* expose a logger, that ppl can use instead of print
"""
# parse the json value if its not an initialized project data instance
automation_run_data = (
automation_run_data
if isinstance(automation_run_data, AutomationRunData)
else AutomationRunData.model_validate_json(automation_run_data)
)
speckle_client = SpeckleClient(
automation_run_data.speckle_server_url,
automation_run_data.speckle_server_url.startswith("https"),
)
speckle_client.authenticate_with_token(speckle_token)
if not speckle_client.account:
msg = (
f"Could not autenticate to {automation_run_data.speckle_server_url}",
"with the provided token",
)
raise ValueError(msg)
server_transport = ServerTransport(
automation_run_data.project_id, speckle_client
)
return cls(automation_run_data, speckle_client, server_transport, speckle_token)
@property
def run_status(self) -> AutomationStatus:
"""Get the status of the automation run."""
return self._automation_result.run_status
@property
def status_message(self) -> Optional[str]:
"""Get the current status message."""
return self._automation_result.status_message
def elapsed(self) -> float:
"""Return the elapsed time in seconds since the initialization time."""
return time.perf_counter() - self._init_time
def receive_version(self) -> Base:
"""Receive the Speckle project version that triggered this automation run."""
# TODO: this is a quick hack to keep implementation consistency. Move to proper receive many versions
version_id = self.automation_run_data.triggers[0].payload.version_id
commit = self.speckle_client.commit.get(
self.automation_run_data.project_id, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
base = operations.receive(
commit.referencedObject, self._server_transport, self._memory_transport
)
print(
f"It took {self.elapsed():.2f} seconds to receive",
f" the speckle version {version_id}",
)
return base
def create_new_version_in_project(
self, root_object: Base, model_name: str, version_message: str = ""
) -> Tuple[str, str]:
"""Save a base model to a new version on the project.
Args:
root_object (Base): The Speckle base object for the new version.
model_id (str): For now please use a `branchName`!
version_message (str): The message for the new version.
"""
branch = self.speckle_client.branch.get(
self.automation_run_data.project_id, model_name, 1
)
if isinstance(branch, Branch):
if not branch.id:
raise ValueError("Cannot use the branch without its id")
matching_trigger = [
t
for t in self.automation_run_data.triggers
if t.payload.model_id == branch.id
]
if matching_trigger:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {matching_trigger[0].payload.model_id}"
)
model_id = branch.id
else:
# we just check if it exists
branch_create = self.speckle_client.branch.create(
self.automation_run_data.project_id,
model_name,
)
if isinstance(branch_create, Exception):
raise branch_create
model_id = branch_create
root_object_id = operations.send(
root_object,
[self._server_transport, self._memory_transport],
use_default_cache=False,
)
version_id = self.speckle_client.commit.create(
stream_id=self.automation_run_data.project_id,
object_id=root_object_id,
branch_name=model_name,
message=version_message,
source_application="SpeckleAutomate",
)
if isinstance(version_id, SpeckleException):
raise version_id
self._automation_result.result_versions.append(version_id)
return model_id, version_id
@property
def context_view(self) -> Optional[str]:
return self._automation_result.result_view
def set_context_view(
self,
# f"{model_id}@{version_id} or {model_id} "
resource_ids: Optional[List[str]] = None,
include_source_model_version: bool = True,
) -> None:
link_resources = (
[
f"{t.payload.model_id}@{t.payload.version_id}"
for t in self.automation_run_data.triggers
]
if include_source_model_version
else []
)
if resource_ids:
link_resources.extend(resource_ids)
if not link_resources:
raise Exception(
"We do not have enough resource ids to compose a context view"
)
self._automation_result.result_view = (
f"/projects/{self.automation_run_data.project_id}"
f"/models/{','.join(link_resources)}"
)
def report_run_status(self) -> None:
"""Report the current run status to the project of this automation."""
query = gql(
"""
mutation AutomateFunctionRunStatusReport(
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
$results: JSONObject
$contextView: String
){
automateFunctionRunStatusReport(input: {
functionRunId: $functionRunId
status: $status
statusMessage: $statusMessage
contextView: $contextView
results: $results
})
}
"""
)
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
object_results = {
"version": 1,
"values": {
"objectResults": self._automation_result.model_dump(by_alias=True)[
"objectResults"
],
"blobIds": self._automation_result.blobs,
},
}
else:
object_results = None
params = {
"functionRunId": self.automation_run_data.function_run_id,
"status": self.run_status.value,
"statusMessage": self._automation_result.status_message,
"results": object_results,
"contextView": self._automation_result.result_view,
}
print(f"Reporting run status with content: {params}")
self.speckle_client.httpclient.execute(query, params)
def store_file_result(self, file_path: Union[Path, str]) -> None:
"""Save a file attached to the project of this automation."""
path_obj = (
Path(file_path).resolve() if isinstance(file_path, str) else file_path
)
class UploadResult(AutomateBase):
blob_id: str
file_name: str
upload_status: int
class BlobUploadResponse(AutomateBase):
upload_results: list[UploadResult]
if not path_obj.exists():
raise ValueError("The given file path doesn't exist")
files = {path_obj.name: open(str(path_obj), "rb")}
url = (
f"{self.automation_run_data.speckle_server_url}api/stream/"
f"{self.automation_run_data.project_id}/blob"
)
data = (
httpx.post(
url,
files=files,
headers={"authorization": f"Bearer {self._speckle_token}"},
)
.raise_for_status()
.json()
)
upload_response = BlobUploadResponse.model_validate(data)
if len(upload_response.upload_results) != 1:
raise ValueError("Expecting one upload result.")
self._automation_result.blobs.extend(
[upload_result.blob_id for upload_result in upload_response.upload_results]
)
def mark_run_failed(self, status_message: str) -> None:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.FAILED, status_message)
def mark_run_exception(self, status_message: str) -> None:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.EXCEPTION, status_message)
def mark_run_success(self, status_message: Optional[str]) -> None:
"""Mark the current run a success with an optional message."""
self._mark_run(AutomationStatus.SUCCEEDED, status_message)
def _mark_run(
self, status: AutomationStatus, status_message: Optional[str]
) -> None:
duration = self.elapsed()
self._automation_result.status_message = status_message
self._automation_result.run_status = status
self._automation_result.elapsed = duration
msg = f"Automation run {status.value} after {duration:.2f} seconds."
print("\n".join([msg, status_message]) if status_message else msg)
def attach_error_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new error case to the run results.
If the error cause has already created an error case,
the error will be extended with a new case refering to the causing objects.
Args:
error_tag (str): A short tag for the error type.
causing_object_ids (str[]): A list of object_id-s that are causing the error
error_messagge (Optional[str]): Optional error message.
metadata: User provided metadata key value pairs
visual_overrides: Case specific 3D visual overrides.
"""
self.attach_result_to_objects(
ObjectResultLevel.ERROR,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_warning_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new warning case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.WARNING,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_success_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new success case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.SUCCESS,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_info_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new info case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.INFO,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_result_to_objects(
self,
level: ObjectResultLevel,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
if isinstance(object_ids, list):
if len(object_ids) < 1:
raise ValueError(
f"Need atleast one object_id to report a(n) {level.value.upper()}"
)
id_list = object_ids
else:
id_list = [object_ids]
print(
f"Created new {level.value.upper()}"
f" category: {category} caused by: {message}"
)
self._automation_result.object_results.append(
ResultCase(
category=category,
level=level,
object_ids=id_list,
message=message,
metadata=metadata,
visual_overrides=visual_overrides,
)
)
-154
View File
@@ -1,154 +0,0 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
import pytest
from gql import gql
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from speckle_automate.schema import AutomationRunData, TestAutomationRunData
from specklepy.api.client import SpeckleClient
class TestAutomationEnvironment(BaseSettings):
"""Get known environment variables from local `.env` file"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="speckle_",
extra="ignore",
)
token: str = Field()
server_url: str = Field()
project_id: str = Field()
automation_id: str = Field()
@pytest.fixture()
def test_automation_environment() -> TestAutomationEnvironment:
return TestAutomationEnvironment()
@pytest.fixture()
def test_automation_token(
test_automation_environment: TestAutomationEnvironment,
) -> str:
"""Provide a speckle token for the test suite."""
return test_automation_environment.token
@pytest.fixture()
def speckle_client(
test_automation_environment: TestAutomationEnvironment,
) -> SpeckleClient:
"""Initialize a SpeckleClient for testing."""
speckle_client = SpeckleClient(
test_automation_environment.server_url,
test_automation_environment.server_url.startswith("https"),
)
speckle_client.authenticate_with_token(test_automation_environment.token)
return speckle_client
def create_test_automation_run(
speckle_client: SpeckleClient, project_id: str, test_automation_id: str
) -> TestAutomationRunData:
"""Create test run to report local test results to"""
query = gql(
"""
mutation CreateTestRun(
$projectId: ID!,
$automationId: ID!
) {
projectMutations {
automationMutations(projectId: $projectId) {
createTestAutomationRun(automationId: $automationId) {
automationRunId
functionRunId
triggers {
payload {
modelId
versionId
}
triggerType
}
}
}
}
}
"""
)
params = {"automationId": test_automation_id, "projectId": project_id}
result = speckle_client.httpclient.execute(query, params)
print(result)
return (
result.get("projectMutations")
.get("automationMutations")
.get("createTestAutomationRun")
)
@pytest.fixture()
def test_automation_run(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> TestAutomationRunData:
return create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
def create_test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
"""Create automation run data for a new run for a given test automation"""
test_automation_run_data = create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
return AutomationRunData(
project_id=test_automation_environment.project_id,
speckle_server_url=test_automation_environment.server_url,
automation_id=test_automation_environment.automation_id,
automation_run_id=test_automation_run_data["automationRunId"],
function_run_id=test_automation_run_data["functionRunId"],
triggers=test_automation_run_data["triggers"],
)
@pytest.fixture()
def test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
return create_test_automation_run_data(speckle_client, test_automation_environment)
def crypto_random_string(length: int) -> str:
"""Generate a semi crypto random string of a given length."""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length)).lower()
__all__ = [
"test_automation_environment",
"test_automation_token",
"speckle_client",
"test_automation_run",
"test_automation_run_data",
]
-197
View File
@@ -1,197 +0,0 @@
"""Function execution module.
Provides mechanisms to execute any function,
that conforms to the AutomateFunction "interface"
"""
import json
import sys
import traceback
from pathlib import Path
from typing import Callable, Optional, Tuple, TypeVar, Union, overload
from pydantic import create_model
from pydantic.json_schema import GenerateJsonSchema
from speckle_automate.automation_context import AutomationContext
from speckle_automate.schema import AutomateBase, AutomationRunData, AutomationStatus
T = TypeVar("T", bound=AutomateBase)
AutomateFunction = Callable[[AutomationContext, T], None]
AutomateFunctionWithoutInputs = Callable[[AutomationContext], None]
def _read_input_data(inputs_location: str) -> str:
input_path = Path(inputs_location)
if not input_path.exists():
raise ValueError(f"Cannot find the function inputs file at {input_path}")
return input_path.read_text()
def _parse_input_data(
input_location: str, input_schema: Optional[type[T]]
) -> Tuple[AutomationRunData, Optional[T], str]:
input_json_string = _read_input_data(input_location)
class FunctionRunData(AutomateBase):
speckle_token: str
automation_run_data: AutomationRunData
function_inputs: None = None
parser_model = FunctionRunData
if input_schema:
parser_model = create_model(
"FunctionRunDataWithInputs",
function_inputs=(input_schema, ...),
__base__=FunctionRunData,
)
input_data = parser_model.model_validate_json(input_json_string)
return (
input_data.automation_run_data,
input_data.function_inputs,
input_data.speckle_token,
)
@overload
def execute_automate_function(
automate_function: AutomateFunction[T],
input_schema: type[T],
) -> None:
...
@overload
def execute_automate_function(
automate_function: AutomateFunctionWithoutInputs,
) -> None:
...
class AutomateGenerateJsonSchema(GenerateJsonSchema):
def generate(self, schema, mode="validation"):
json_schema = super().generate(schema, mode=mode)
json_schema["$schema"] = self.schema_dialect
return json_schema
def execute_automate_function(
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
input_schema: Optional[type[T]] = None,
):
"""Runs the provided automate function with the input schema."""
# first arg is the python file name, we do not need that
args = sys.argv[1:]
if len(args) != 2:
raise ValueError("Incorrect number of arguments specified need 2")
# we rely on a command name convention to decide what to do.
# this is here, so that the function authors do not see any of this
command, argument = args
if command == "generate_schema":
path = Path(argument)
schema = json.dumps(
input_schema.model_json_schema(
by_alias=True, schema_generator=AutomateGenerateJsonSchema
)
if input_schema
else {}
)
path.write_text(schema)
elif command == "run":
automation_run_data, function_inputs, speckle_token = _parse_input_data(
argument, input_schema
)
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
if function_inputs:
automation_context = run_function(
automation_context,
automate_function, # type: ignore
function_inputs, # type: ignore
)
else:
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
automation_context = run_function(
automation_context,
automate_function, # type: ignore
)
# if we've gotten this far, the execution should technically be completed as expected
# thus exiting with 0 is the schemantically correct thing to do
exit_code = (
1 if automation_context.run_status == AutomationStatus.EXCEPTION else 0
)
exit(exit_code)
else:
raise NotImplementedError(f"Command: '{command}' is not supported.")
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunction[T],
inputs: T,
) -> AutomationContext:
...
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunctionWithoutInputs,
) -> AutomationContext:
...
def run_function(
automation_context: AutomationContext,
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
inputs: Optional[T] = None,
) -> AutomationContext:
"""Run the provided function with the automate sdk context."""
automation_context.report_run_status()
try:
# avoiding complex type gymnastics here on the internals.
# the external type overloads make this correct
if inputs:
automate_function(automation_context, inputs) # type: ignore
else:
automate_function(automation_context) # type: ignore
# the function author forgot to mark the function success
if automation_context.run_status not in [
AutomationStatus.FAILED,
AutomationStatus.SUCCEEDED,
AutomationStatus.EXCEPTION,
]:
automation_context.mark_run_success(
"WARNING: Automate assumed a success status,"
" but it was not marked as so by the function."
)
except Exception:
trace = traceback.format_exc()
print(trace)
automation_context.mark_run_exception(
"Function error. Check the automation run logs for details."
)
finally:
if not automation_context.context_view:
automation_context.set_context_view()
automation_context.report_run_status()
return automation_context
-98
View File
@@ -1,98 +0,0 @@
""""""
from enum import Enum
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from stringcase import camelcase
class AutomateBase(BaseModel):
"""Use this class as a base model for automate related DTO."""
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
"""Represents the version creation trigger payload."""
model_id: str
version_id: str
class VersionCreationTrigger(AutomateBase):
"""Represents a single version creation trigger for the automation run."""
trigger_type: Literal["versionCreation"]
payload: VersionCreationTriggerPayload
class AutomationRunData(BaseModel):
"""Values of the project / model that triggered the run of this function."""
project_id: str
speckle_server_url: str
automation_id: str
automation_run_id: str
function_run_id: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
class TestAutomationRunData(BaseModel):
"""Values of the run created in the test automation for local test results."""
automation_run_id: str
function_run_id: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
class AutomationStatus(str, Enum):
"""Set the status of the automation."""
INITIALIZING = "INITIALIZING"
RUNNING = "RUNNING"
FAILED = "FAILED"
SUCCEEDED = "SUCCEEDED"
EXCEPTION = "EXCEPTION"
class ObjectResultLevel(str, Enum):
"""Possible status message levels for object reports."""
SUCCESS = "SUCCESS"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
class ResultCase(AutomateBase):
"""A result case."""
category: str
level: ObjectResultLevel
object_ids: List[str]
message: Optional[str]
metadata: Optional[Dict[str, Any]]
visual_overrides: Optional[Dict[str, Any]]
class AutomationResult(AutomateBase):
"""Schema accepted by the Speckle server as a result for an automation run."""
elapsed: float = 0
result_view: Optional[str] = None
result_versions: List[str] = Field(default_factory=list)
blobs: List[str] = Field(default_factory=list)
run_status: AutomationStatus = AutomationStatus.RUNNING
status_message: Optional[str] = None
object_results: list[ResultCase] = Field(default_factory=list)
-3
View File
@@ -1,3 +0,0 @@
from specklepy import objects
__all__ = ["objects"]
+36 -75
View File
@@ -1,24 +1,30 @@
from deprecated import deprecated
import re
from typing import Dict
from warnings import warn
from specklepy.api.credentials import Account
from deprecated import deprecated
from gql import Client
from gql.transport.exceptions import TransportServerError
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.websockets import WebsocketsTransport
from specklepy.api import resources
from specklepy.api.credentials import Account, get_account_from_token
from specklepy.api.resources import (
ActiveUserResource,
ModelResource,
OtherUserResource,
ProjectInviteResource,
ProjectResource,
SubscriptionResource,
VersionResource,
user,
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
class SpeckleClient(CoreSpeckleClient):
@@ -26,7 +32,7 @@ class SpeckleClient(CoreSpeckleClient):
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `app.speckle.systems`.
or you can use our public server `speckle.xyz`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
@@ -37,7 +43,7 @@ class SpeckleClient(CoreSpeckleClient):
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
@@ -52,19 +58,13 @@ class SpeckleClient(CoreSpeckleClient):
```
"""
DEFAULT_HOST = "app.speckle.systems"
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
) -> None:
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
super().__init__(
host=host,
use_ssl=use_ssl,
verify_certificate=verify_certificate,
)
self.account = Account()
@@ -72,62 +72,29 @@ class SpeckleClient(CoreSpeckleClient):
self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
try:
server_version = self.server.version()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = ActiveUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project = ProjectResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project_invite = ProjectInviteResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.model = ModelResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.version = VersionResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.subscription = SubscriptionResource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
# todo: why doesn't this take a server version
)
# Deprecated Resources
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.other_user = other_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = active_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
@@ -164,9 +131,7 @@ class SpeckleClient(CoreSpeckleClient):
Arguments:
token {str} -- an api token
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate_deprecated"}
)
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate_deprecated"})
return super().authenticate(token)
def authenticate_with_token(self, token: str) -> None:
@@ -178,9 +143,7 @@ class SpeckleClient(CoreSpeckleClient):
Arguments:
token {str} -- an api token
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate With Token"}
)
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate With Token"})
return super().authenticate_with_token(token)
def authenticate_with_account(self, account: Account) -> None:
@@ -192,7 +155,5 @@ class SpeckleClient(CoreSpeckleClient):
account {Account} -- the account object which can be found with
`get_default_account` or `get_local_accounts`
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate With Account"}
)
metrics.track(metrics.SDK, self.account, {"name": "Client Authenticate With Account"})
return super().authenticate_with_account(account)
+17 -13
View File
@@ -1,14 +1,20 @@
import os
from typing import List, Optional
# 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 StreamWrapper # noqa: F401
from specklepy.core.api.credentials import Account, UserInfo # noqa: F401
from specklepy.core.api.credentials import (
get_account_from_token as core_get_account_from_token,
)
from specklepy.core.api.credentials import get_local_accounts as core_get_local_accounts
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from specklepy.api.models import ServerInfo
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.credentials import (Account, UserInfo,
StreamWrapper, # deprecated
get_local_accounts as core_get_local_accounts,
get_account_from_token as core_get_account_from_token)
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
@@ -29,12 +35,11 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
(acc for acc in accounts if acc.isDefault),
accounts[0] if accounts else None,
),
{"name": "Get Local Accounts"},
{"name": "Get Local Accounts"}
)
return accounts
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
"""
Gets this environment's default account if any. If there is no default,
@@ -56,8 +61,7 @@ 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:
@@ -69,5 +73,5 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
"""
account = core_get_account_from_token(token, server_url)
metrics.track(metrics.SDK, account, {"name": "Get Account From Token"})
metrics.track( metrics.SDK, account, {"name": "Get Account From Token"} )
return account
+17 -73
View File
@@ -1,74 +1,18 @@
from specklepy.core.api.host_applications import (
ARCGIS,
ARCHICAD,
AUTOCAD,
BLENDER,
CIVIL,
CSIBRIDGE,
DXF,
DYNAMO,
ETABS,
EXCEL,
GRASSHOPPER,
GSA,
MICROSTATION,
NET,
OPENBUILDINGS,
OPENRAIL,
OPENROADS,
OTHER,
POWERBI,
PYTHON,
QGIS,
REVIT,
RHINO,
SAFE,
SAP2000,
SKETCHUP,
TEKLASTRUCTURES,
TOPSOLID,
UNITY,
UNREAL,
HostApplication,
HostAppVersion,
_app_name_host_app_mapping,
get_host_app_from_string,
)
from dataclasses import dataclass
from enum import Enum
from unicodedata import name
# re-exporting stuff from the moved api module
__all__ = [
"ARCGIS",
"ARCHICAD",
"AUTOCAD",
"BLENDER",
"CIVIL",
"CSIBRIDGE",
"DXF",
"DYNAMO",
"ETABS",
"EXCEL",
"GRASSHOPPER",
"GSA",
"MICROSTATION",
"NET",
"OPENBUILDINGS",
"OPENRAIL",
"OPENROADS",
"OTHER",
"POWERBI",
"PYTHON",
"QGIS",
"REVIT",
"RHINO",
"SAFE",
"SAP2000",
"SKETCHUP",
"TEKLASTRUCTURES",
"TOPSOLID",
"UNITY",
"UNREAL",
"HostApplication",
"HostAppVersion",
"_app_name_host_app_mapping",
"get_host_app_from_string",
]
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.host_applications import (HostApplication, HostAppVersion,
get_host_app_from_string,
_app_name_host_app_mapping,
RHINO,GRASSHOPPER,REVIT,DYNAMO,UNITY,GSA,
CIVIL,AUTOCAD,MICROSTATION,OPENROADS,
OPENRAIL,OPENBUILDINGS,ETABS,SAP2000,CSIBRIDGE,
SAFE,TEKLASTRUCTURES,DXF,EXCEL,UNREAL,POWERBI,
BLENDER,QGIS,ARCGIS,SKETCHUP,ARCHICAD,TOPSOLID,
PYTHON,NET,OTHER)
if __name__ == "__main__":
print(HostAppVersion.v)
+12
View File
@@ -0,0 +1,12 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.models import (Collaborator, Commit,
Commits, Object, Branch, Branches,
Stream, Streams, User, LimitedUser,
PendingStreamCollaborator, Activity,
ActivityCollection, ServerInfo)
-35
View File
@@ -1,35 +0,0 @@
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.models import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
LimitedUser,
Object,
PendingStreamCollaborator,
ServerInfo,
Stream,
Streams,
User,
)
__all__ = [
"Activity",
"ActivityCollection",
"Branch",
"Branches",
"Collaborator",
"Commit",
"Commits",
"LimitedUser",
"Object",
"PendingStreamCollaborator",
"ServerInfo",
"Stream",
"Streams",
"User",
]
+8 -5
View File
@@ -1,12 +1,16 @@
from typing import List, Optional
from specklepy.core.api.operations import deserialize as core_deserialize
from specklepy.core.api.operations import receive as _untracked_receive
from specklepy.core.api.operations import send as core_send
from specklepy.core.api.operations import serialize as core_serialize
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.transports.sqlite import SQLiteTransport
from specklepy.core.api.operations import (send as core_send,
receive as _untracked_receive,
serialize as core_serialize,
deserialize as core_deserialize)
def send(
@@ -70,7 +74,6 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
metrics.track(metrics.SDK, custom_props={"name": "Serialize"})
return core_serialize(base, write_transports)
def deserialize(
obj_string: str, read_transport: Optional[AbstractTransport] = None
) -> Base:
+20 -6
View File
@@ -1,8 +1,21 @@
from typing import Any, Optional, Tuple
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from gql.client import Client
from gql.transport.exceptions import TransportQueryError
from graphql import DocumentNode
from specklepy.api.credentials import Account
from specklepy.logging.exceptions import (
GraphQLException,
SpeckleException,
UnsupportedException,
)
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.resource import ResourceBase as CoreResourceBase
@@ -16,9 +29,10 @@ class ResourceBase(CoreResourceBase):
server_version: Optional[Tuple[Any, ...]] = None,
) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=name,
server_version=server_version,
account = account,
basepath = basepath,
client = client,
name = name,
server_version = server_version
)
+8 -40
View File
@@ -1,41 +1,9 @@
from specklepy.api.resources.current.active_user_resource import ActiveUserResource
from specklepy.api.resources.current.model_resource import ModelResource
from specklepy.api.resources.current.other_user_resource import OtherUserResource
from specklepy.api.resources.current.project_invite_resource import (
ProjectInviteResource,
)
from specklepy.api.resources.current.project_resource import ProjectResource
from specklepy.api.resources.current.server_resource import ServerResource
from specklepy.api.resources.current.subscription_resource import SubscriptionResource
from specklepy.api.resources.current.version_resource import VersionResource
from specklepy.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
import pkgutil
import sys
from importlib import import_module
__all__ = [
"ActiveUserResource",
"ModelResource",
"OtherUserResource",
"ProjectInviteResource",
"ProjectResource",
"ServerResource",
"SubscriptionResource",
"VersionResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
for _, name, _ in pkgutil.iter_modules(__path__):
imported_module = import_module("." + name, package=__name__)
if hasattr(imported_module, "Resource"):
setattr(sys.modules[__name__], name, imported_module)
+120
View File
@@ -0,0 +1,120 @@
from datetime import datetime, timezone
from typing import List, Optional
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.active_user import Resource as CoreResource
class Resource(CoreResource):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = User
def get(self) -> User:
"""Gets the profile of a user. If no id argument is provided,
will return the current authenticated user's profile
(as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
metrics.track(metrics.SDK, custom_props={"name": "User Active Get"})
return super().get()
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns @deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT):
bool -- True if your profile was updated successfully
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Update"})
return super().update(name, company, bio, avatar)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's
activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
return super().activity(limit, action_type, before, after, cursor)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invites All Get"})
return super().get_all_pending_invites()
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
@@ -1,15 +1,12 @@
from typing import Optional, Union
from typing import Optional
from deprecated import deprecated
from gql import gql
from specklepy.api.models import Branch
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.branch import Resource as CoreResource
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.branch import Resource as CoreResource
class Resource(CoreResource):
@@ -23,7 +20,6 @@ class Resource(CoreResource):
)
self.schema = Branch
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self, stream_id: str, name: str, description: str = "No description provided"
) -> str:
@@ -39,10 +35,7 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
return super().create(stream_id, name, description)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(
self, stream_id: str, name: str, commits_limit: int = 10
) -> Union[Branch, None, SpeckleException]:
def get(self, stream_id: str, name: str, commits_limit: int = 10):
"""Get a branch by name from a stream
Arguments:
@@ -56,7 +49,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch Get"})
return super().get(stream_id, name, commits_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
@@ -71,7 +63,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch List"})
return super().list(stream_id, branches_limit, commits_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
stream_id: str,
@@ -93,7 +84,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Branch Update"})
return super().update(stream_id, branch_id, name, description)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
@@ -1,15 +1,12 @@
from typing import List, Optional, Union
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.api.models import Commit
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.commit import Resource as CoreResource
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.commit import Resource as CoreResource
class Resource(CoreResource):
@@ -23,7 +20,6 @@ class Resource(CoreResource):
)
self.schema = Commit
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, commit_id: str) -> Commit:
"""
Gets a commit given a stream and the commit id
@@ -38,7 +34,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Commit Get"})
return super().get(stream_id, commit_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, limit: int = 10) -> List[Commit]:
"""
Get a list of commits on a given stream
@@ -53,7 +48,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Commit List"})
return super().list(stream_id, limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
stream_id: str,
@@ -61,8 +55,8 @@ class Resource(CoreResource):
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
parents: List[str] = None,
) -> str:
"""
Creates a commit on a branch
@@ -82,11 +76,8 @@ class Resource(CoreResource):
str -- the id of the created commit
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Create"})
return super().create(
stream_id, object_id, branch_name, message, source_application, parents
)
return super().create(stream_id, object_id, branch_name, message, source_application, parents)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
Update a commit
@@ -103,7 +94,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Commit Update"})
return super().update(stream_id, commit_id, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
Delete a commit
@@ -119,7 +109,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Commit Delete"})
return super().delete(stream_id, commit_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def received(
self,
stream_id: str,
@@ -1,147 +0,0 @@
from datetime import datetime
from typing import List, Optional, overload
from deprecated import deprecated
from specklepy.core.api.inputs.project_inputs import UserProjectsFilter
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.models import (
PendingStreamCollaborator,
Project,
ResourceCollection,
User,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import ActiveUserResource as CoreResource
from specklepy.logging import metrics
class ActiveUserResource(CoreResource):
"""API Access class for users. This class provides methods to get and update
the user profile, fetch user activity, and manage pending stream invitations."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = User
def get(self) -> Optional[User]:
metrics.track(metrics.SDK, self.account, {"name": "Active User Get"})
return super().get()
@deprecated("Use UserUpdateInput overload", version=FE1_DEPRECATION_VERSION)
@overload
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
) -> User:
...
@overload
def update(self, *, input: UserUpdateInput) -> User:
...
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
*,
input: Optional[UserUpdateInput] = None,
) -> User:
metrics.track(metrics.SDK, self.account, {"name": "Active User Update"})
if isinstance(input, UserUpdateInput):
return super()._update(input=input)
else:
return super()._update(
input=UserUpdateInput(
name=name,
company=company,
bio=bio,
avatar=avatar,
)
)
def get_projects(
self,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[UserProjectsFilter] = None,
) -> ResourceCollection[Project]:
metrics.track(metrics.SDK, self.account, {"name": "Active User Get Projects"})
return super().get_projects(limit=limit, cursor=cursor, filter=filter)
def get_project_invites(self) -> List[PendingStreamCollaborator]:
metrics.track(
metrics.SDK, self.account, {"name": "Active User Get Project Invites"}
)
return super().get_project_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Fetches collection the current authenticated user's activity
as filtered by given parameters
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
Args:
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): Filter results to a single action type.
before (Optional[datetime]): Latest cutoff for activity to include.
after (Optional[datetime]): Oldest cutoff for an activity to include.
cursor (Optional[datetime]): Timestamp cursor for pagination.
Returns:
Activity collection, filtered according to the provided parameters.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
return super().activity(limit, action_type, before, after, cursor)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Fetches all of the current user's pending stream invitations.
Returns:
List[PendingStreamCollaborator]: A list of pending stream invitations.
"""
metrics.track(
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
)
return super().get_all_pending_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Fetches a specific pending invite for the current user on a given stream.
Args:
stream_id (str): The ID of the stream to look for invites on.
token (Optional[str]): The token of the invite to look for (optional).
Returns:
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
@@ -1,74 +0,0 @@
from typing import Optional
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
ModelVersionsFilter,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
from specklepy.core.api.resources import ModelResource as CoreResource
from specklepy.logging import metrics
class ModelResource(CoreResource):
"""API Access class for models"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, model_id: str, project_id: str) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Get"})
return super().get(model_id, project_id)
def get_with_versions(
self,
model_id: str,
project_id: str,
*,
versions_limit: int = 25,
versions_cursor: Optional[str] = None,
versions_filter: Optional[ModelVersionsFilter] = None,
) -> ModelWithVersions:
metrics.track(metrics.SDK, self.account, {"name": "Model Get With Versions"})
return super().get_with_versions(
model_id,
project_id,
versions_limit=versions_limit,
versions_cursor=versions_cursor,
versions_filter=versions_filter,
)
def get_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ResourceCollection[Model]:
metrics.track(metrics.SDK, self.account, {"name": "Model Get Models"})
return super().get_models(
project_id,
models_limit=models_limit,
models_cursor=models_cursor,
models_filter=models_filter,
)
def create(self, input: CreateModelInput) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Create"})
return super().create(input)
def delete(self, input: DeleteModelInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Model Delete"})
return super().delete(input)
def update(self, input: UpdateModelInput) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Update"})
return super().update(input)
@@ -1,104 +0,0 @@
from datetime import datetime
from typing import List, Optional, Union
from deprecated import deprecated
from specklepy.core.api.models import (
ActivityCollection,
LimitedUser,
UserSearchResultCollection,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import OtherUserResource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class OtherUserResource(CoreResource):
"""
Provides API access to other users' profiles and activities on the platform.
This class enables fetching limited information about users, searching for users by name or email,
and accessing user activity logs with appropriate privacy and access control measures in place.
"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=(server_version,),
)
self.schema = LimitedUser
def get(self, id: str) -> Optional[LimitedUser]:
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
def user_search(
self,
query: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
archived: bool = False,
emailOnly: bool = False,
) -> UserSearchResultCollection:
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().user_search(
query, limit=limit, cursor=cursor, archived=archived, emailOnly=emailOnly
)
@deprecated(reason="Use user_search instead", version=FE1_DEPRECATION_VERSION)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""
Searches for users by name or email.
The search requires a minimum query length of 3 characters.
Args:
search_query (str): The search string.
limit (int): Maximum number of search results to return.
Returns:
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
query or an exception if the query is too short.
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters."
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
Args:
user_id (str): The ID of the user whose activities are being requested.
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): A specific type of activity to filter.
before (Optional[datetime]): Latest timestamp to include activities before.
after (Optional[datetime]): Earliest timestamp to include activities after.
cursor (Optional[datetime]): Timestamp for pagination cursor.
Returns:
ActivityCollection: A collection of user activities filtered according to specified criteria.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@@ -1,54 +0,0 @@
from typing import Any, Optional, Tuple
from gql import Client
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import (
ProjectInviteCreateInput,
ProjectInviteUseInput,
)
from specklepy.core.api.models import PendingStreamCollaborator, ProjectWithTeam
from specklepy.core.api.resources import ProjectInviteResource as CoreResource
from specklepy.logging import metrics
class ProjectInviteResource(CoreResource):
"""API Access class for project invites"""
def __init__(
self,
account: Account,
basepath: str,
client: Client,
server_version: Optional[Tuple[Any, ...]],
) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def create(
self, project_id: str, input: ProjectInviteCreateInput
) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Create"})
return super().create(project_id, input)
def use(self, input: ProjectInviteUseInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Use"})
return super().use(input)
def get(
self, project_id: str, token: Optional[str]
) -> Optional[PendingStreamCollaborator]:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Get"})
return super().get(project_id, token)
def cancel(
self,
project_id: str,
invite_id: str,
) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Cancel"})
return super().cancel(project_id, invite_id)
@@ -1,63 +0,0 @@
from typing import Optional
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
from specklepy.core.api.resources import ProjectResource as CoreResource
from specklepy.logging import metrics
class ProjectResource(CoreResource):
"""API Access class for projects"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, project_id: str) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Get "})
return super().get(project_id)
def get_with_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ProjectWithModels:
metrics.track(metrics.SDK, self.account, {"name": "Project Get With Models"})
return super().get_with_models(
project_id,
models_limit=models_limit,
models_cursor=models_cursor,
models_filter=models_filter,
)
def get_with_team(self, project_id: str) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Get With Team"})
return super().get_with_team(project_id)
def create(self, input: ProjectCreateInput) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Create"})
return super().create(input)
def update(self, input: ProjectUpdateInput) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Update"})
return super().update(input)
def delete(self, project_id: str) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Project Delete"})
return super().delete(project_id)
def update_role(self, input: ProjectUpdateRoleInput) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Update Role"})
return super().update_role(input)
@@ -1,64 +0,0 @@
from typing import Callable, Optional, Sequence
from pydantic import BaseModel
from typing_extensions import TypeVar
from specklepy.core.api.models import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
)
from specklepy.core.api.resources import SubscriptionResource as CoreResource
from specklepy.logging import metrics
TEventArgs = TypeVar("TEventArgs", bound=BaseModel)
class SubscriptionResource(CoreResource):
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
async def user_projects_updated(
self, callback: Callable[[UserProjectsUpdatedMessage], None]
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Models Updated"}
)
return await super().user_projects_updated(callback)
async def project_models_updated(
self,
callback: Callable[[ProjectModelsUpdatedMessage], None],
id: str,
*,
model_ids: Optional[Sequence[str]] = None,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Models Updated"}
)
return await super().project_models_updated(callback, id, model_ids=model_ids)
async def project_updated(
self,
callback: Callable[[ProjectUpdatedMessage], None],
id: str,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Updated"}
)
return await super().project_updated(callback, id)
async def project_versions_updated(
self,
callback: Callable[[ProjectVersionsUpdatedMessage], None],
id: str,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Versions Updated"}
)
return await super().project_versions_updated(callback, id)
@@ -1,63 +0,0 @@
from typing import Optional
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.inputs.version_inputs import (
CreateVersionInput,
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
from specklepy.core.api.models import ResourceCollection, Version
from specklepy.core.api.resources import VersionResource as CoreResource
from specklepy.logging import metrics
class VersionResource(CoreResource):
"""API Access class for model versions"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, version_id: str, project_id: str) -> Version:
metrics.track(metrics.SDK, self.account, {"name": "Version Get"})
return super().get(version_id, project_id)
def get_versions(
self,
model_id: str,
project_id: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[ModelVersionsFilter] = None,
) -> ResourceCollection[Version]:
metrics.track(metrics.SDK, self.account, {"name": "Version Get Versions"})
return super().get_versions(
model_id, project_id, limit=limit, cursor=cursor, filter=filter
)
def create(self, input: CreateVersionInput) -> str:
metrics.track(metrics.SDK, self.account, {"name": "Version Create"})
return super().create(input)
def update(self, input: UpdateVersionInput) -> Version:
metrics.track(metrics.SDK, self.account, {"name": "Version Update"})
return super().update(input)
def move_to_model(self, input: MoveVersionsInput) -> str:
metrics.track(metrics.SDK, self.account, {"name": "Version Move To Model"})
return super().move_to_model(input)
def delete(self, input: DeleteVersionsInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Version Delete"})
return super().delete(input)
def received(self, input: MarkReceivedVersionInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Version Received"})
return super().received(input)
@@ -1,9 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import ActiveUserResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(ActiveUserResource):
"""Renamed to ActiveUserResource"""
@@ -1,11 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import OtherUserResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to OtherUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(OtherUserResource):
"""
Renamed to OtherUserResource
"""
@@ -1,9 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import ServerResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""Renamed to ServerResource"""
@@ -1,15 +1,14 @@
from typing import Dict, List
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.object import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.api.resource import ResourceBase
from specklepy.objects.base import Base
from specklepy.logging import metrics
from specklepy.core.api.resources.object import Resource as CoreResource
class Resource(CoreResource):
"""API Access class for objects"""
@@ -22,7 +21,6 @@ class Resource(CoreResource):
)
self.schema = Base
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, object_id: str) -> Base:
"""
Get a stream object
@@ -37,7 +35,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Object Get"})
return super().get(stream_id, object_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(self, stream_id: str, objects: List[Dict]) -> str:
"""
Not advised - generally, you want to use `operations.send()`.
@@ -61,3 +58,4 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "Object Create"})
return super().create(stream_id, objects)
+87
View File
@@ -0,0 +1,87 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from gql import gql
from specklepy.api.models import ActivityCollection, LimitedUser
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.core.api.resources.other_user import Resource as CoreResource
class Resource(CoreResource):
"""API Access class for other users, that are not the currently active user."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = LimitedUser
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
Arguments:
id {str} -- the user id
Returns:
LimitedUser -- the retrieved profile of another user
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""Searches for user by name or email. The search query must be at least
3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[LimitedUser] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@@ -1,11 +1,17 @@
import re
from typing import Any, Dict, List, Tuple
from gql import gql
from specklepy.api.models import ServerInfo
from specklepy.core.api.resources import ServerResource as CoreResource
from specklepy.api.resource import ResourceBase
from specklepy.logging import metrics
from specklepy.logging.exceptions import GraphQLException
from specklepy.core.api.resources.server import Resource as CoreResource
class ServerResource(CoreResource):
class Resource(CoreResource):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
@@ -67,4 +73,4 @@ class ServerResource(CoreResource):
bool -- True if the token was successfully deleted
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Revoke Token"})
return super().revoke_token(token)
return super().revoke_token(token)
@@ -1,15 +1,15 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.api.models import PendingStreamCollaborator, Stream
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.stream import Resource as CoreResource
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, Stream
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 Resource as CoreResource
class Resource(CoreResource):
@@ -25,7 +25,6 @@ class Resource(CoreResource):
self.schema = Stream
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream:
"""Get the specified stream from the server
@@ -40,7 +39,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Get"})
return super().get(id, branch_limit, commit_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_limit: int = 10) -> List[Stream]:
"""Get a list of the user's streams
@@ -53,7 +51,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream List"})
return super().list(stream_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
name: str = "Anonymous Python Stream",
@@ -74,7 +71,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Create"})
return super().create(name, description, is_public)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
id: str,
@@ -97,7 +93,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Update"})
return super().update(id, name, description, is_public)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, id: str) -> bool:
"""Delete a stream given its id
@@ -110,7 +105,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Delete"})
return super().delete(id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def search(
self,
search_query: str,
@@ -132,7 +126,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Search"})
return super().search(search_query, limit, branch_limit, commit_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream.
@@ -147,7 +140,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Favorite"})
return super().favorite(stream_id, favorited)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(
self, stream_id: str
) -> List[PendingStreamCollaborator]:
@@ -166,7 +158,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Get"})
return super().get_all_pending_invites(stream_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite(
self,
stream_id: str,
@@ -194,7 +185,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Create"})
return super().invite(stream_id, email, user_id, role, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_batch(
self,
stream_id: str,
@@ -221,7 +211,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Batch Create"})
return super().invite_batch(stream_id, emails, user_ids, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite
@@ -237,7 +226,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Cancel"})
return super().invite_cancel(stream_id, invite_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite
@@ -255,7 +243,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Invite Use"})
return super().invite_use(stream_id, token, accept)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream
@@ -269,14 +256,9 @@ class Resource(CoreResource):
Returns:
bool -- True if the operation was successful
"""
metrics.track(
metrics.SDK,
self.account,
{"name": "Stream Permission Update", "role": role},
)
metrics.track(metrics.SDK, self.account, {"name": "Stream Permission Update", "role": role})
return super().update_permission(stream_id, user_id, role)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
@@ -290,7 +272,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Stream Permission Revoke"})
return super().revoke_permission(stream_id, user_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
stream_id: str,
@@ -320,3 +301,4 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Activity"})
return super().activity(stream_id, action_type, limit, before, after, cursor)
@@ -1,17 +1,27 @@
from functools import wraps
from typing import Callable, Dict, List, Optional, Union
from deprecated import deprecated
from gql import gql
from graphql import DocumentNode
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.current.subscription_resource import check_wsclient
from specklepy.core.api.resources.deprecated.subscriptions import (
Resource as CoreResource,
)
from specklepy.api.resource import ResourceBase
from specklepy.api.resources.stream import Stream
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
from specklepy.core.api.resources.subscriptions import Resource as CoreResource
def check_wsclient(function):
@wraps(function)
async def check_wsclient_wrapper(self, *args, **kwargs):
if self.client is None:
raise SpeckleException(
"You must authenticate before you can subscribe to events"
)
else:
return await function(self, *args, **kwargs)
return check_wsclient_wrapper
class Resource(CoreResource):
@@ -24,7 +34,6 @@ class Resource(CoreResource):
client=client,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_added(self, callback: Optional[Callable] = None):
"""Subscribes to new stream added event for your profile.
@@ -40,7 +49,6 @@ class Resource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Added"})
return super().stream_added(callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_updated(self, id: str, callback: Optional[Callable] = None):
"""
@@ -56,12 +64,9 @@ class Resource(CoreResource):
Returns:
Stream -- the update stream
"""
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Updated"}
)
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Updated"})
return super().stream_updated(id, callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_removed(self, callback: Optional[Callable] = None):
"""Subscribes to stream removed event for your profile.
@@ -78,12 +83,9 @@ class Resource(CoreResource):
Returns:
dict -- dict containing 'id' of stream removed and optionally 'revokedBy'
"""
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Removed"}
)
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Removed"})
return super().stream_removed(callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def subscribe(
self,
@@ -1,13 +1,17 @@
from datetime import datetime
from datetime import datetime, timezone
from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.api.resource import ResourceBase
from specklepy.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.deprecated.user import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
from specklepy.core.api.resources.user import Resource as CoreResource
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = (
"The user resource is deprecated, please use the active_user or other_user"
@@ -42,7 +46,7 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "User Get_deprecated"})
return super().get(id)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search(
self, search_query: str, limit: int = 25
@@ -79,7 +83,7 @@ class Resource(CoreResource):
Returns:
bool -- True if your profile was updated successfully
"""
# metrics.track(metrics.USER, self.account, {"name": "update"})
#metrics.track(metrics.USER, self.account, {"name": "update"})
metrics.track(metrics.SDK, self.account, {"name": "User Update_deprecated"})
return super().update(name, company, bio, avatar)
@@ -114,6 +118,7 @@ class Resource(CoreResource):
"""
metrics.track(metrics.SDK, self.account, {"name": "User Activity_deprecated"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
@@ -125,11 +130,10 @@ class Resource(CoreResource):
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(
metrics.SDK, self.account, {"name": "User GetAllInvites_deprecated"}
)
#metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(metrics.SDK, self.account, {"name": "User GetAllInvites_deprecated"})
return super().get_all_pending_invites()
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite(
@@ -148,6 +152,7 @@ class Resource(CoreResource):
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
#metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(metrics.SDK, self.account, {"name": "User GetInvite_deprecated"})
return super().get_pending_invite(stream_id, token)
+14 -8
View File
@@ -1,9 +1,17 @@
from urllib.parse import unquote, urlparse
from warnings import warn
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import Account
from specklepy.core.api.wrapper import StreamWrapper as CoreStreamWrapper
from specklepy.logging import metrics
from specklepy.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
)
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
from specklepy.logging import metrics
from specklepy.core.api.wrapper import StreamWrapper as CoreStreamWrapper
class StreamWrapper(CoreStreamWrapper):
"""
@@ -22,7 +30,7 @@ class StreamWrapper(CoreStreamWrapper):
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
@@ -43,7 +51,7 @@ class StreamWrapper(CoreStreamWrapper):
_account: Account = None
def __init__(self, url: str) -> None:
super().__init__(url=url)
super().__init__(url = url)
def get_account(self, token: str = None) -> Account:
"""
@@ -82,7 +90,5 @@ class StreamWrapper(CoreStreamWrapper):
ServerTransport -- constructed for this stream
with a pre-authenticated client
"""
metrics.track(
metrics.SDK, custom_props={"name": "Stream Wrapper Get Transport"}
)
metrics.track(metrics.SDK, custom_props={"name": "Stream Wrapper Get Transport"})
return super().get_transport(token)
+41 -88
View File
@@ -11,20 +11,15 @@ from gql.transport.websockets import WebsocketsTransport
from specklepy.core.api import resources
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.core.api.resources import (
ActiveUserResource,
ModelResource,
OtherUserResource,
ProjectInviteResource,
ProjectResource,
ServerResource,
SubscriptionResource,
VersionResource,
user,
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
@@ -35,7 +30,7 @@ class SpeckleClient:
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `app.speckle.systems`.
or you can use our public server `speckle.xyz`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
@@ -46,7 +41,7 @@ class SpeckleClient:
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
client = SpeckleClient(host="speckle.xyz") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
@@ -61,17 +56,10 @@ class SpeckleClient:
```
"""
DEFAULT_HOST = "app.speckle.systems"
DEFAULT_HOST = "speckle.xyz"
USE_SSL = True
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
connection_retries: int = 3,
connection_timeout: int = 10,
) -> None:
def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
ws_protocol = "ws"
http_protocol = "http"
@@ -86,17 +74,9 @@ class SpeckleClient:
self.graphql = f"{self.url}/graphql"
self.ws_url = f"{ws_protocol}://{host}/graphql"
self.account = Account()
self.verify_certificate = verify_certificate
self.connection_retries = connection_retries
self.connection_timeout = connection_timeout
self.httpclient = Client(
transport=RequestsHTTPTransport(
url=self.graphql,
verify=self.verify_certificate,
retries=self.connection_retries,
timeout=self.connection_timeout,
)
transport=RequestsHTTPTransport(url=self.graphql, verify=True, retries=3)
)
self.wsclient = None
@@ -135,7 +115,8 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.authenticate_with_account(get_account_from_token(token))
self.authenticate_with_token(token)
self._set_up_client()
def authenticate_with_token(self, token: str) -> None:
"""
@@ -146,7 +127,7 @@ class SpeckleClient:
Arguments:
token {str} -- an api token
"""
self.account = Account.from_token(token, self.url)
self.account = get_account_from_token(token, self.url)
self._set_up_client()
def authenticate_with_account(self, account: Account) -> None:
@@ -169,7 +150,7 @@ class SpeckleClient:
"apollographql-client-version": metrics.HOST_APP_VERSION,
}
httptransport = RequestsHTTPTransport(
url=self.graphql, headers=headers, verify=self.verify_certificate, retries=3
url=self.graphql, headers=headers, verify=True, retries=3
)
wstransport = WebsocketsTransport(
url=self.ws_url,
@@ -181,81 +162,53 @@ class SpeckleClient:
self._init_resources()
try:
_ = self.active_user.get()
except SpeckleException as ex:
if isinstance(ex.exception, TransportServerError):
if ex.exception.code == 403:
warn(
SpeckleWarning(
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
)
)
user_or_error = self.active_user.get()
if isinstance(user_or_error, SpeckleException):
if isinstance(user_or_error.exception, TransportServerError):
raise user_or_error.exception
else:
raise ex
raise user_or_error
except TransportServerError as ex:
if ex.code == 403:
warn(
SpeckleWarning(
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
)
)
else:
raise ex
def execute_query(self, query: str) -> Dict:
return self.httpclient.execute(query)
def _init_resources(self) -> None:
self.server = ServerResource(
self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
try:
server_version = self.server.version()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = ActiveUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project = ProjectResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project_invite = ProjectInviteResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.model = ModelResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.version = VersionResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.subscription = SubscriptionResource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
)
# Deprecated Resources
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.other_user = other_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = active_user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
+3 -26
View File
@@ -1,22 +1,21 @@
import os
from pathlib import Path
from typing import List, Optional
from urllib.parse import urlparse
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from specklepy.core.api.models import ServerInfo
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport
class UserInfo(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
email: Optional[str] = None
company: Optional[str] = None
avatar: Optional[str] = None
id: Optional[str] = None
class Account(BaseModel):
@@ -111,7 +110,7 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
if not default:
default = accounts[0]
default.isDefault = True
# metrics.initialise_tracker(default)
#metrics.initialise_tracker(default)
return default
@@ -144,28 +143,6 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
return Account.from_token(token, server_url)
def get_accounts_for_server(host: str) -> List[Account]:
all_accounts = get_local_accounts()
filtered: List[Account] = []
for acc in all_accounts:
moved_from = (
acc.serverInfo.migration.movedFrom if acc.serverInfo.migration else None
)
if moved_from and host == urlparse(moved_from).netloc:
filtered.append(acc)
for acc in all_accounts:
if any([x for x in filtered if x.userInfo.id == acc.userInfo.id]):
continue
if host == urlparse(acc.serverInfo.url).netloc:
filtered.append(acc)
return filtered
class StreamWrapper:
def __init__(self, url: str = None) -> None:
raise SpeckleException(
-29
View File
@@ -1,29 +0,0 @@
from enum import Enum
class ProjectVisibility(str, Enum):
PRIVATE = "PRIVATE"
PUBLIC = "PUBLIC"
UNLISTEd = "UNLISTED"
class UserProjectsUpdatedMessageType(str, Enum):
ADDED = "ADDED"
REMOVED = "REMOVED"
class ProjectModelsUpdatedMessageType(str, Enum):
CREATED = "CREATED"
DELETED = "DELETED"
UPDATED = "UPDATED"
class ProjectUpdatedMessageType(str, Enum):
DELETED = "DELETED"
UPDATED = "UPDATED"
class ProjectVersionsUpdatedMessageType(str, Enum):
CREATED = "CREATED"
DELETED = "DELETED"
UPDATED = "UPDATED"
@@ -1,26 +0,0 @@
from typing import Optional, Sequence
from pydantic import BaseModel
class CreateModelInput(BaseModel):
name: str
description: Optional[str] = None
projectId: str
class DeleteModelInput(BaseModel):
id: str
projectId: str
class UpdateModelInput(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
projectId: str
class ModelVersionsFilter(BaseModel):
priorityIds: Sequence[str]
priorityIdsOnly: Optional[bool] = None
@@ -1,52 +0,0 @@
from typing import Optional, Sequence
from pydantic import BaseModel
from specklepy.core.api.enums import ProjectVisibility
class ProjectCreateInput(BaseModel):
name: Optional[str]
description: Optional[str]
visibility: Optional[ProjectVisibility]
class ProjectInviteCreateInput(BaseModel):
email: Optional[str]
role: Optional[str]
serverRole: Optional[str]
userId: Optional[str]
class ProjectInviteUseInput(BaseModel):
accept: bool
projectId: str
token: str
class ProjectModelsFilter(BaseModel):
contributors: Optional[Sequence[str]] = None
excludeIds: Optional[Sequence[str]] = None
ids: Optional[Sequence[str]] = None
onlyWithVersions: Optional[bool] = None
search: Optional[str] = None
sourceApps: Optional[Sequence[str]] = None
class ProjectUpdateInput(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
allowPublicComments: Optional[bool] = None
visibility: Optional[ProjectVisibility] = None
class ProjectUpdateRoleInput(BaseModel):
userId: str
projectId: str
role: Optional[str]
class UserProjectsFilter(BaseModel):
search: str
onlyWithRole: Optional[Sequence[str]] = None
@@ -1,10 +0,0 @@
from typing import Optional
from pydantic import BaseModel
class UserUpdateInput(BaseModel):
avatar: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
name: Optional[str] = None
@@ -1,37 +0,0 @@
from typing import Optional, Sequence
from pydantic import BaseModel
class UpdateVersionInput(BaseModel):
versionId: str
projectId: str
message: Optional[str]
class MoveVersionsInput(BaseModel):
targetModelName: str
versionIds: Sequence[str]
projectId: str
class DeleteVersionsInput(BaseModel):
versionIds: Sequence[str]
projectId: str
class CreateVersionInput(BaseModel):
objectId: str
modelId: str
projectId: str
message: Optional[str] = None
sourceApplication: Optional[str] = "py"
totalChildrenCount: Optional[int] = None
parents: Optional[Sequence[str]] = None
class MarkReceivedVersionInput(BaseModel):
versionId: str
projectId: str
sourceApplication: str
message: Optional[str] = None
@@ -1,14 +1,9 @@
from datetime import datetime
from typing import List, Optional
from deprecated import deprecated
from pydantic import BaseModel, Field
FE1_DEPRECATION_REASON = "Stream/Branch/Commit API is now deprecated, Use the new Project/Model/Version API functions in Client}"
FE1_DEPRECATION_VERSION = "2.20"
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Collaborator(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
@@ -16,7 +11,6 @@ class Collaborator(BaseModel):
avatar: Optional[str] = None
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Commit(BaseModel):
id: Optional[str] = None
message: Optional[str] = None
@@ -41,14 +35,12 @@ class Commit(BaseModel):
return self.__repr__()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Commits(BaseModel):
totalCount: Optional[int] = None
cursor: Optional[datetime] = None
items: List[Commit] = []
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Object(BaseModel):
id: Optional[str] = None
speckleType: Optional[str] = None
@@ -57,7 +49,6 @@ class Object(BaseModel):
createdAt: Optional[datetime] = None
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Branch(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
@@ -65,14 +56,12 @@ class Branch(BaseModel):
commits: Optional[Commits] = None
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Branches(BaseModel):
totalCount: Optional[int] = None
cursor: Optional[datetime] = None
items: List[Branch] = []
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Stream(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
@@ -99,14 +88,67 @@ class Stream(BaseModel):
return self.__repr__()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Streams(BaseModel):
totalCount: Optional[int] = None
cursor: Optional[datetime] = None
items: List[Stream] = []
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class User(BaseModel):
id: Optional[str] = None
email: Optional[str] = None
name: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
streams: Optional[Streams] = None
def __repr__(self):
return (
f"User( id: {self.id}, name: {self.name}, email: {self.email}, company:"
f" {self.company} )"
)
def __str__(self) -> str:
return self.__repr__()
class LimitedUser(BaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
name: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
class PendingStreamCollaborator(BaseModel):
id: Optional[str] = None
inviteId: Optional[str] = None
streamId: Optional[str] = None
streamName: Optional[str] = None
title: Optional[str] = None
role: Optional[str] = None
invitedBy: Optional[User] = None
user: Optional[User] = None
token: Optional[str] = None
def __repr__(self):
return (
f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId:"
f" {self.streamId}, role: {self.role}, title: {self.title}, invitedBy:"
f" {self.user.name if self.user else None})"
)
def __str__(self) -> str:
return self.__repr__()
class Activity(BaseModel):
actionType: Optional[str] = None
info: Optional[dict] = None
@@ -127,7 +169,6 @@ class Activity(BaseModel):
return self.__repr__()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class ActivityCollection(BaseModel):
totalCount: Optional[int] = None
items: Optional[List[Activity]] = None
@@ -142,3 +183,16 @@ class ActivityCollection(BaseModel):
def __str__(self) -> str:
return self.__repr__()
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
url: Optional[str] = None
description: Optional[str] = None
adminContact: Optional[str] = None
canonicalUrl: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
-71
View File
@@ -1,71 +0,0 @@
from specklepy.core.api.models.current import (
AuthStrategy,
LimitedUser,
Model,
ModelWithVersions,
PendingStreamCollaborator,
Project,
ProjectCollaborator,
ProjectCommentCollection,
ProjectWithModels,
ProjectWithTeam,
ResourceCollection,
ServerConfiguration,
ServerInfo,
ServerMigration,
User,
UserSearchResultCollection,
Version,
)
from specklepy.core.api.models.deprecated import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
Object,
Stream,
Streams,
)
from specklepy.core.api.models.subscription_messages import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
)
__all__ = [
"User",
"ResourceCollection",
"ServerMigration",
"AuthStrategy",
"ServerConfiguration",
"ServerInfo",
"LimitedUser",
"PendingStreamCollaborator",
"ProjectCollaborator",
"Version",
"Model",
"ModelWithVersions",
"Project",
"ProjectWithModels",
"ProjectWithTeam",
"ProjectCommentCollection",
"UserSearchResultCollection",
"UserProjectsUpdatedMessage",
"ProjectModelsUpdatedMessage",
"ProjectUpdatedMessage",
"ProjectVersionsUpdatedMessage",
"Collaborator",
"Commit",
"Commits",
"Object",
"Branch",
"Branches",
"Stream",
"Streams",
"Activity",
"ActivityCollection",
]
-171
View File
@@ -1,171 +0,0 @@
from datetime import datetime
from typing import Generic, List, Optional, TypeVar
from pydantic import BaseModel
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.models.deprecated import Streams
T = TypeVar("T")
class User(BaseModel):
id: str
email: Optional[str] = None
name: str
bio: Optional[str] = None
company: Optional[str] = None
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
streams: Optional["Streams"] = None
def __repr__(self):
return (
f"User( id: {self.id}, name: {self.name}, email: {self.email}, company:"
f" {self.company} )"
)
def __str__(self) -> str:
return self.__repr__()
class ResourceCollection(BaseModel, Generic[T]):
totalCount: int
items: List[T]
cursor: Optional[str] = None
class ServerMigration(BaseModel):
movedFrom: Optional[str]
movedTo: Optional[str]
class AuthStrategy(BaseModel):
color: Optional[str]
icon: str
id: str
name: str
url: str
class ServerConfiguration(BaseModel):
blobSizeLimitBytes: int
objectMultipartUploadSizeLimitBytes: int
objectSizeLimitBytes: int
# Keeping this one all Optionals at the minute, because its used both as a deserialization model for GQL and Account Management
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
url: Optional[str] = None
adminContact: Optional[str] = None
description: Optional[str] = None
canonicalUrl: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
# TODO separate gql model from account management model
class LimitedUser(BaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
name: str
bio: Optional[str]
company: Optional[str]
avatar: Optional[str]
verified: Optional[bool]
role: Optional[str]
class PendingStreamCollaborator(BaseModel):
id: str
inviteId: str
streamId: Optional[str] = None
projectId: str
streamName: Optional[str] = None
projectName: str
title: str
role: str
invitedBy: LimitedUser
user: Optional[LimitedUser] = None
token: Optional[str]
def __repr__(self):
return (
f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId:"
f" {self.streamId}, role: {self.role}, title: {self.title}, invitedBy:"
f" {self.user.name if self.user else None})"
)
def __str__(self) -> str:
return self.__repr__()
class ProjectCollaborator(BaseModel):
id: str
role: str
user: LimitedUser
class Version(BaseModel):
authorUser: Optional[LimitedUser]
createdAt: datetime
id: str
message: Optional[str]
previewUrl: str
referencedObject: str
sourceApplication: Optional[str]
class Model(BaseModel):
author: LimitedUser
createdAt: datetime
description: Optional[str]
displayName: str
id: str
name: str
previewUrl: Optional[str]
updatedAt: datetime
class ModelWithVersions(Model):
versions: ResourceCollection[Version]
class Project(BaseModel):
allowPublicComments: bool
createdAt: datetime
description: Optional[str]
id: str
name: str
role: Optional[str]
sourceApps: List[str]
updatedAt: datetime
visibility: ProjectVisibility
workspaceId: Optional[str]
class ProjectWithModels(Project):
models: ResourceCollection[Model]
class ProjectWithTeam(Project):
invitedTeam: List[PendingStreamCollaborator]
team: List[ProjectCollaborator]
class ProjectCommentCollection(ResourceCollection[T], Generic[T]):
totalArchivedCount: int
class UserSearchResultCollection(BaseModel):
items: List[LimitedUser]
cursor: Optional[str] = None
@@ -1,36 +0,0 @@
from typing import Optional
from pydantic import BaseModel
from specklepy.core.api.enums import (
ProjectModelsUpdatedMessageType,
ProjectUpdatedMessageType,
ProjectVersionsUpdatedMessageType,
UserProjectsUpdatedMessageType,
)
from specklepy.core.api.models.current import Model, Project, Version
class UserProjectsUpdatedMessage(BaseModel):
id: str
type: UserProjectsUpdatedMessageType
project: Optional[Project]
class ProjectModelsUpdatedMessage(BaseModel):
id: str
type: ProjectModelsUpdatedMessageType
model: Optional[Model]
class ProjectUpdatedMessage(BaseModel):
id: str
type: ProjectUpdatedMessageType
project: Optional[Project]
class ProjectVersionsUpdatedMessage(BaseModel):
id: str
type: ProjectVersionsUpdatedMessageType
modelId: Optional[str]
version: Optional[Version]
+1 -1
View File
@@ -1,6 +1,6 @@
from typing import List, Optional
# from specklepy.logging import metrics
#from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
+1 -35
View File
@@ -1,10 +1,9 @@
from threading import Lock
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from gql.client import Client
from gql.transport.exceptions import TransportQueryError
from graphql import DocumentNode
from pydantic import BaseModel
from specklepy.core.api.credentials import Account
from specklepy.logging.exceptions import (
@@ -15,8 +14,6 @@ from specklepy.logging.exceptions import (
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
T = TypeVar("T", bound=BaseModel)
class ResourceBase(object):
def __init__(
@@ -46,35 +43,6 @@ class ResourceBase(object):
response = response[key]
return response
def make_request_and_parse_response(
self,
schema: Type[T],
query: DocumentNode,
variables: Optional[Dict[str, Any]] = None,
) -> T:
try:
with self.__lock:
response = self.client.execute(query, variable_values=variables)
except TransportQueryError as ex:
raise GraphQLException(
message=(
f"Failed to execute the GraphQL {self.name} request. Errors:"
f" {ex.errors}"
),
errors=ex.errors,
data=ex.data,
) from ex
except Exception as ex:
raise SpeckleException(
message=(
f"Failed to execute the GraphQL {self.name} request. Inner"
f" exception: {ex}"
),
exception=ex,
) from ex
return schema.model_validate(response)
def _parse_response(self, response: Union[dict, list, None], schema=None):
"""Try to create a class instance from the response"""
if response is None:
@@ -101,8 +69,6 @@ class ResourceBase(object):
parse_response: bool = True,
) -> Any:
"""Executes the GraphQL query"""
# This method has quite complex and ambiguous typing, and counter-intuitive error handling
# We are going to phase it out in favour of `make_request_and_parse_response`
try:
with self.__lock:
response = self.client.execute(query, variable_values=params)
@@ -1,43 +0,0 @@
from specklepy.core.api.resources.current.active_user_resource import ActiveUserResource
from specklepy.core.api.resources.current.model_resource import ModelResource
from specklepy.core.api.resources.current.other_user_resource import OtherUserResource
from specklepy.core.api.resources.current.project_invite_resource import (
ProjectInviteResource,
)
from specklepy.core.api.resources.current.project_resource import ProjectResource
from specklepy.core.api.resources.current.server_resource import ServerResource
from specklepy.core.api.resources.current.subscription_resource import (
SubscriptionResource,
)
from specklepy.core.api.resources.current.version_resource import VersionResource
from specklepy.core.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
__all__ = [
"ActiveUserResource",
"ModelResource",
"OtherUserResource",
"ProjectInviteResource",
"ProjectResource",
"ServerResource",
"SubscriptionResource",
"VersionResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
@@ -1,31 +1,17 @@
from datetime import datetime, timezone
from typing import List, Optional, overload
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.inputs.project_inputs import UserProjectsFilter
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Project,
ResourceCollection,
User,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
from specklepy.logging.exceptions import GraphQLException
from specklepy.logging.exceptions import SpeckleException
NAME = "active_user"
class ActiveUserResource(ResourceBase):
"""API Access class for the active user"""
class Resource(ResourceBase):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -37,75 +23,38 @@ class ActiveUserResource(ResourceBase):
)
self.schema = User
def get(self) -> Optional[User]:
"""Gets the currently active user profile (as extracted from the authorization header)
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 requested user, or none if no authentication token is provided to the Client
User -- the retrieved user
"""
QUERY = gql(
query = gql(
"""
query User {
data:activeUser {
id
email
name
bio
company
avatar
verified
role
}
}
"""
)
variables = {}
return self.make_request_and_parse_response(
DataResponse[Optional[User]], QUERY, variables
).data
def _update(self, input: UserUpdateInput) -> User:
QUERY = gql(
"""
mutation ActiveUserMutations($input: UserUpdateInput!) {
data:activeUserMutations {
data:update(user: $input) {
id
email
name
bio
company
avatar
verified
role
activeUser {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
}
"""
"""
)
variables = {"input": input.model_dump(warnings="error")}
params = {}
return self.make_request_and_parse_response(
DataResponse[DataResponse[User]], QUERY, variables
).data.data
@deprecated("Use UserUpdateInput overload", version=FE1_DEPRECATION_VERSION)
@overload
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
) -> User:
...
@overload
def update(self, *, input: UserUpdateInput) -> User:
...
return self.make_request(query=query, params=params, return_type="activeUser")
def update(
self,
@@ -113,125 +62,40 @@ class ActiveUserResource(ResourceBase):
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
*,
input: Optional[UserUpdateInput] = None,
) -> User:
if isinstance(input, UserUpdateInput):
return self._update(input=input)
else:
return self._update(
input=UserUpdateInput(
name=name,
company=company,
bio=bio,
avatar=avatar,
):
"""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"
)
)
def get_projects(
self,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[UserProjectsFilter] = None,
) -> ResourceCollection[Project]:
QUERY = gql(
"""
query User($limit : Int!, $cursor: String, $filter: UserProjectsFilter) {
data:activeUser {
data:projects(limit: $limit, cursor: $cursor, filter: $filter) {
totalCount
cursor
items {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
}
}
}
"""
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
variables = {
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error") if filter else None,
}
response = self.make_request_and_parse_response(
DataResponse[Optional[DataResponse[ResourceCollection[Project]]]],
QUERY,
variables,
)
if response.data is None:
raise GraphQLException(
"GraphQL response indicated that the ActiveUser could not be found"
)
return response.data.data
def get_project_invites(self) -> List[PendingStreamCollaborator]:
QUERY = gql(
"""
query ProjectInvites {
data:activeUser {
data:projectInvites {
id
inviteId
invitedBy {
avatar
bio
company
id
name
role
verified
}
projectId
projectName
role
title
token
user {
id
name
bio
company
verified
avatar
role
}
}
}
}
"""
)
variables = {}
response = self.make_request_and_parse_response(
DataResponse[Optional[DataResponse[List[PendingStreamCollaborator]]]],
QUERY,
variables,
)
if response.data is None:
raise GraphQLException(
"GraphQL response indicated that the ActiveUser could not be found"
)
return response.data.data
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 20,
@@ -239,7 +103,7 @@ class ActiveUserResource(ResourceBase):
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
@@ -310,7 +174,6 @@ class ActiveUserResource(ResourceBase):
schema=ActivityCollection,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
@@ -331,18 +194,13 @@ class ActiveUserResource(ResourceBase):
inviteId
streamId
streamName
projectId
projectName
title
role
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
@@ -355,7 +213,6 @@ class ActiveUserResource(ResourceBase):
schema=PendingStreamCollaborator,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
@@ -380,21 +237,15 @@ class ActiveUserResource(ResourceBase):
streamInvite(streamId: $streamId, token: $token) {
id
token
inviteId
streamId
streamName
projectId
projectName
title
role
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
@@ -1,24 +1,15 @@
from typing import Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
Branch,
)
from specklepy.core.api.models import Branch
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "branch"
class Resource(ResourceBase):
"""
API Access class for branches
Branch resource is deprecated, please use model resource instead
"""
"""API Access class for branches"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
@@ -29,7 +20,6 @@ class Resource(ResourceBase):
)
self.schema = Branch
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self, stream_id: str, name: str, description: str = "No description provided"
) -> str:
@@ -49,8 +39,6 @@ class Resource(ResourceBase):
}
"""
)
if len(name) < 3:
return SpeckleException(message="Branch Name must be at least 3 characters")
params = {
"branch": {
"streamId": stream_id,
@@ -63,7 +51,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="branchCreate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, name: str, commits_limit: int = 10):
"""Get a branch by name from a stream
@@ -111,7 +98,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "branch"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
@@ -167,7 +153,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "branches", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
stream_id: str,
@@ -209,7 +194,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="branchUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
@@ -1,24 +1,15 @@
from typing import List, Optional, Union
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
Commit,
)
from specklepy.core.api.models import Commit
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "commit"
class Resource(ResourceBase):
"""
API Access class for commits
Commit resource is deprecated, please use version resource instead
"""
"""API Access class for commits"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
@@ -29,7 +20,6 @@ class Resource(ResourceBase):
)
self.schema = Commit
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, commit_id: str) -> Commit:
"""
Gets a commit given a stream and the commit id
@@ -68,7 +58,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "commit"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, limit: int = 10) -> List[Commit]:
"""
Get a list of commits on a given stream
@@ -110,7 +99,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "commits", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
stream_id: str,
@@ -118,8 +106,8 @@ class Resource(ResourceBase):
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
parents: List[str] = None,
) -> str:
"""
Creates a commit on a branch
@@ -160,7 +148,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitCreate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
Update a commit
@@ -188,7 +175,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
Delete a commit
@@ -213,7 +199,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitDelete", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def received(
self,
stream_id: str,
@@ -1,278 +0,0 @@
from typing import Optional
from gql import gql
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
ModelVersionsFilter,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "model"
class ModelResource(ResourceBase):
"""API Access class for models"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
def get(self, model_id: str, project_id: str) -> Model:
QUERY = gql(
"""
query ModelGet($modelId: String!, $projectId: String!) {
data:project(id: $projectId) {
data:model(id: $modelId) {
id
name
previewUrl
updatedAt
description
displayName
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
}
}
"""
)
variables = {
"modelId": model_id,
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Model]], QUERY, variables
).data.data
def get_with_versions(
self,
model_id: str,
project_id: str,
*,
versions_limit: int = 25,
versions_cursor: Optional[str] = None,
versions_filter: Optional[ModelVersionsFilter] = None,
) -> ModelWithVersions:
QUERY = gql(
"""
query ModelGetWithVersions($modelId: String!, $projectId: String!, $versionsLimit: Int!, $versionsCursor: String, $versionsFilter: ModelVersionsFilter) {
data:project(id: $projectId) {
data:model(id: $modelId) {
id
name
previewUrl
updatedAt
versions(limit: $versionsLimit, cursor: $versionsCursor, filter: $versionsFilter) {
items {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
avatar
id
name
bio
company
verified
role
}
}
totalCount
cursor
}
description
displayName
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"modelId": model_id,
"versionsLimit": versions_limit,
"versionsCursor": versions_cursor,
"versionsFilter": versions_filter.model_dump(warnings="error")
if versions_filter
else None,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[ModelWithVersions]], QUERY, variables
).data.data
def get_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ResourceCollection[Model]:
QUERY = gql(
"""
query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) {
data:project(id: $projectId) {
data:models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) {
items {
id
name
previewUrl
updatedAt
displayName
description
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
totalCount
cursor
}
}
}
"""
)
variables = {
"projectId": project_id,
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": models_filter.model_dump(warnings="error")
if models_filter
else None,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[ResourceCollection[Model]]], QUERY, variables
).data.data
def create(self, input: CreateModelInput) -> Model:
QUERY = gql(
"""
mutation ModelCreate($input: CreateModelInput!) {
data:modelMutations {
data:create(input: $input) {
id
displayName
name
description
createdAt
updatedAt
previewUrl
author {
avatar
bio
company
id
name
role
verified
}
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Model]], QUERY, variables
).data.data
def delete(self, input: DeleteModelInput) -> bool:
QUERY = gql(
"""
mutation ModelDelete($input: DeleteModelInput!) {
data:modelMutations {
data:delete(input: $input)
}
}
"""
)
variables = {"input": input.model_dump(warnings="error")}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
).data.data
def update(self, input: UpdateModelInput) -> Model:
QUERY = gql(
"""
mutation ModelUpdate($input: UpdateModelInput!) {
data:modelMutations {
data:update(input: $input) {
id
name
displayName
description
createdAt
updatedAt
previewUrl
author {
avatar
bio
company
id
name
role
verified
}
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Model]], QUERY, variables
).data.data
@@ -1,254 +0,0 @@
from typing import Any, Optional, Tuple
from gql import Client, gql
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import (
ProjectInviteCreateInput,
ProjectInviteUseInput,
)
from specklepy.core.api.models import PendingStreamCollaborator, ProjectWithTeam
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "project_invite"
class ProjectInviteResource(ResourceBase):
"""API Access class for project invites"""
def __init__(
self,
account: Account,
basepath: str,
client: Client,
server_version: Optional[Tuple[Any, ...]],
) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
def create(
self, project_id: str, input: ProjectInviteCreateInput
) -> ProjectWithTeam:
QUERY = gql(
"""
mutation ProjectInviteCreate($projectId: ID!, $input: ProjectInviteCreateInput!) {
data:projectMutations {
data:invites {
data:create(projectId: $projectId, input: $input) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
workspaceId
sourceApps
team {
id
role
user {
id
name
bio
company
avatar
verified
role
}
}
invitedTeam {
id
inviteId
projectId
projectName
title
role
token
user {
id
name
bio
company
avatar
verified
role
}
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[ProjectWithTeam]]], QUERY, variables
).data.data.data
def use(self, input: ProjectInviteUseInput) -> bool:
QUERY = gql(
"""
mutation ProjectInviteUse($input: ProjectInviteUseInput!) {
data:projectMutations {
data:invites {
data:use(input: $input)
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[bool]]], QUERY, variables
).data.data.data
def get(
self, project_id: str, token: Optional[str]
) -> Optional[PendingStreamCollaborator]:
"""Returns: The invite, or None if no invite exists"""
QUERY = gql(
"""
query ProjectInvite($projectId: String!, $token: String) {
data:projectInvite(projectId: $projectId, token: $token) {
id
inviteId
invitedBy {
avatar
bio
company
id
name
role
verified
}
projectId
projectName
role
title
token
user {
avatar
bio
company
id
name
role
verified
}
}
}
"""
)
variables = {
"projectId": project_id,
"token": token,
}
return self.make_request_and_parse_response(
DataResponse[Optional[PendingStreamCollaborator]], QUERY, variables
).data
def cancel(
self,
project_id: str,
invite_id: str,
) -> ProjectWithTeam:
QUERY = gql(
"""
mutation ProjectInviteCancel($projectId: ID!, $inviteId: String!) {
data:projectMutations {
data:invites {
data:cancel(projectId: $projectId, inviteId: $inviteId) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
team {
id
role
user {
id
name
bio
company
avatar
verified
role
}
}
invitedTeam {
id
inviteId
projectId
projectName
title
role
token
user {
id
name
bio
company
avatar
verified
role
}
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"inviteId": invite_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[ProjectWithTeam]]], QUERY, variables
).data.data.data
@@ -1,336 +0,0 @@
from typing import Optional
from gql import gql
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "project"
class ProjectResource(ResourceBase):
"""API Access class for projects"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
def get(self, project_id: str) -> Project:
QUERY = gql(
"""
query Project($projectId: String!) {
data:project(id: $projectId) {
allowPublicComments
createdAt
description
id
name
role
sourceApps
updatedAt
visibility
workspaceId
}
}
"""
)
variables = {
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[Project], QUERY, variables
).data
def get_with_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ProjectWithModels:
QUERY = gql(
"""
query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) {
data:project(id: $projectId) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) {
items {
id
name
previewUrl
updatedAt
displayName
description
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
cursor
totalCount
}
}
}
"""
)
variables = {
"projectId": project_id,
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": models_filter.model_dump(warnings="error")
if models_filter
else None,
}
return self.make_request_and_parse_response(
DataResponse[ProjectWithModels], QUERY, variables
).data
def get_with_team(self, project_id: str) -> ProjectWithTeam:
QUERY = gql(
"""
query ProjectGetWithTeam($projectId: String!) {
data:project(id: $projectId) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
workspaceId
sourceApps
team {
id
role
user {
id
name
bio
company
avatar
verified
role
}
}
invitedTeam {
id
inviteId
projectId
projectName
title
role
token
user {
id
name
bio
company
avatar
verified
role
}
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
workspaceId
}
}
"""
)
variables = {
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[ProjectWithTeam], QUERY, variables
).data
def create(self, input: ProjectCreateInput) -> Project:
QUERY = gql(
"""
mutation ProjectCreate($input: ProjectCreateInput) {
data:projectMutations {
data:create(input: $input) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Project]], QUERY, variables
).data.data
def update(self, input: ProjectUpdateInput) -> Project:
QUERY = gql(
"""
mutation ProjectUpdate($input: ProjectUpdateInput!) {
data:projectMutations{
data:update(update: $input) {
allowPublicComments
createdAt
description
id
name
role
sourceApps
updatedAt
visibility
workspaceId
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Project]], QUERY, variables
).data.data
def delete(self, project_id: str) -> bool:
QUERY = gql(
"""
mutation ProjectDelete($projectId: String!) {
data:projectMutations {
data:delete(id: $projectId)
}
}
"""
)
variables = {
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
).data.data
def update_role(self, input: ProjectUpdateRoleInput) -> ProjectWithTeam:
QUERY = gql(
"""
mutation ProjectUpdateRole($input: ProjectUpdateRoleInput!) {
data:projectMutations {
data:updateRole(input: $input) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
team {
id
role
user {
id
name
bio
company
avatar
verified
role
}
}
invitedTeam {
id
inviteId
projectId
projectName
title
role
token
user {
id
name
bio
company
avatar
verified
role
}
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[ProjectWithTeam]], QUERY, variables
).data.data
@@ -1,218 +0,0 @@
from functools import wraps
from typing import Any, Callable, Dict, Optional, Sequence, Type
from gql import gql
from graphql import DocumentNode
from pydantic import BaseModel
from typing_extensions import TypeVar
from specklepy.core.api.models import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
from specklepy.logging.exceptions import SpeckleException
NAME = "subscribe"
TEventArgs = TypeVar("TEventArgs", bound=BaseModel)
def check_wsclient(function):
@wraps(function)
async def check_wsclient_wrapper(self, *args, **kwargs):
if self.client is None:
raise SpeckleException(
"You must authenticate before you can subscribe to events"
)
else:
return await function(self, *args, **kwargs)
return check_wsclient_wrapper
class SubscriptionResource(ResourceBase):
"""API Access class for subscriptions"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
)
async def user_projects_updated(
self, callback: Callable[[UserProjectsUpdatedMessage], None]
) -> None:
QUERY = gql(
"""
subscription UserProjectsUpdated {
data:userProjectsUpdated {
id
project {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
type
}
}
"""
)
await self.subscribe_2(
DataResponse[UserProjectsUpdatedMessage],
QUERY,
None,
callback=lambda d: callback(d.data),
)
async def project_models_updated(
self,
callback: Callable[[ProjectModelsUpdatedMessage], None],
id: str,
model_ids: Optional[Sequence[str]] = None,
) -> None:
QUERY = gql(
"""
subscription ProjectModelsUpdated($id: String!, $modelIds: [String!]) {
data:projectModelsUpdated(id: $id, modelIds: $modelIds) {
id
model {
id
name
previewUrl
updatedAt
description
displayName
createdAt
author {
avatar
bio
company
id
name
role
verified
}
}
type
}
}
"""
)
variables = {"id": id, "modelIds": model_ids}
await self.subscribe_2(
DataResponse[ProjectModelsUpdatedMessage],
QUERY,
variables,
callback=lambda d: callback(d.data),
)
async def project_updated(
self,
callback: Callable[[ProjectUpdatedMessage], None],
id: str,
) -> None:
QUERY = gql(
"""
subscription ProjectUpdated($id: String!) {
data:projectUpdated(id: $id) {
id
project {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
type
}
}
"""
)
variables = {"id": id}
await self.subscribe_2(
DataResponse[ProjectUpdatedMessage],
QUERY,
variables,
callback=lambda d: callback(d.data),
)
async def project_versions_updated(
self,
callback: Callable[[ProjectVersionsUpdatedMessage], None],
id: str,
) -> None:
QUERY = gql(
"""
subscription ProjectVersionsUpdated($id: String!) {
data:projectVersionsUpdated(id: $id) {
id
modelId
type
version {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
}
}
"""
)
variables = {"id": id}
await self.subscribe_2(
DataResponse[ProjectVersionsUpdatedMessage],
QUERY,
variables,
callback=lambda d: callback(d.data),
)
@check_wsclient
async def subscribe_2(
self,
response_type: Type[TEventArgs],
query: DocumentNode,
variables: Optional[Dict[str, Any]],
callback: Callable[[TEventArgs], None],
) -> None:
async with self.client as session:
self.session = session
gen = session.subscribe(query, variable_values=variables)
async for res in gen:
event_arg = response_type.model_validate(res)
callback(event_arg)
@@ -1,234 +0,0 @@
from typing import Optional
from gql import gql
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.inputs.version_inputs import (
CreateVersionInput,
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
from specklepy.core.api.models import ResourceCollection, Version
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "model"
class VersionResource(ResourceBase):
"""API Access class for model versions"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
def get(self, version_id: str, project_id: str) -> Version:
QUERY = gql(
"""
query VersionGet($projectId: String!, $versionId: String!) {
data:project(id: $projectId) {
data:version(id: $versionId) {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"versionId": version_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Version]], QUERY, variables
).data.data
def get_versions(
self,
model_id: str,
project_id: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[ModelVersionsFilter] = None,
) -> ResourceCollection[Version]:
QUERY = gql(
"""
query VersionGetVersions($projectId: String!, $modelId: String!, $limit: Int!, $cursor: String, $filter: ModelVersionsFilter) {
data:project(id: $projectId) {
data:model(id: $modelId) {
data:versions(limit: $limit, cursor: $cursor, filter: $filter) {
items {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
cursor
totalCount
}
}
}
}
"""
)
variables = {
"projectId": project_id,
"modelId": model_id,
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error") if filter else None,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[ResourceCollection[Version]]]],
QUERY,
variables,
).data.data.data
def create(self, input: CreateVersionInput) -> str:
QUERY = gql(
"""
mutation Create($input: CreateVersionInput!) {
data:versionMutations {
data:create(input: $input) {
data:id
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[str]]], QUERY, variables
).data.data.data
def update(self, input: UpdateVersionInput) -> Version:
QUERY = gql(
"""
mutation VersionUpdate($input: UpdateVersionInput!) {
data:versionMutations {
data:update(input: $input) {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
}
}
"""
)
variables = {"input": input.model_dump(warnings="error")}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Version]], QUERY, variables
).data.data
def move_to_model(self, input: MoveVersionsInput) -> str:
QUERY = gql(
"""
mutation VersionMoveToModel($input: MoveVersionsInput!) {
data:versionMutations {
data:moveToModel(input: $input) {
data:id
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[str]]], QUERY, variables
).data.data.data
def delete(self, input: DeleteVersionsInput) -> bool:
QUERY = gql(
"""
mutation VersionDelete($input: DeleteVersionsInput!) {
data:versionMutations {
data:delete(input: $input)
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
).data.data
def received(self, input: MarkReceivedVersionInput) -> bool:
QUERY = gql(
"""
mutation MarkReceived($input: MarkReceivedVersionInput!) {
data:versionMutations {
data:markReceived(input: $input)
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
).data.data
@@ -1,15 +0,0 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import ActiveUserResource
@deprecated(
reason="Class renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION
)
class Resource(ActiveUserResource):
"""
Class renamed to ActiveUserResource
"""
pass
@@ -1,15 +0,0 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import OtherUserResource
@deprecated(
reason="Class renamed to OtherUserResource", version=FE1_DEPRECATION_VERSION
)
class Resource(OtherUserResource):
"""
Class renamed to OtherUserResource
"""
pass
@@ -1,11 +0,0 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import ServerResource
NAME = "server"
@deprecated(reason="Renamed to ServerResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""API Access class for the server"""
@@ -1,2 +0,0 @@
schema: https://app.speckle.systems/graphql
documents: '**/*.graphql'
@@ -1,26 +1,16 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import (
ActivityCollection,
LimitedUser,
UserSearchResultCollection,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.models import ActivityCollection, LimitedUser
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
from specklepy.logging.exceptions import SpeckleException
NAME = "other_user"
class OtherUserResource(ResourceBase):
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:
@@ -33,7 +23,7 @@ class OtherUserResource(ResourceBase):
)
self.schema = LimitedUser
def get(self, id: str) -> Optional[LimitedUser]:
def get(self, id: str) -> LimitedUser:
"""
Gets the profile of another user.
@@ -43,81 +33,26 @@ class OtherUserResource(ResourceBase):
Returns:
LimitedUser -- the retrieved profile of another user
"""
QUERY = gql(
query = gql(
"""
query LimitedUser($id: String!) {
data:otherUser(id: $id){
id
name
bio
company
avatar
verified
role
query OtherUser($id: String!) {
otherUser(id: $id) {
id
name
bio
company
avatar
verified
role
}
}
}
"""
)
variables = {"id": id}
params = {"id": id}
return self.make_request_and_parse_response(
DataResponse[Optional[LimitedUser]], QUERY, variables
).data
return self.make_request(query=query, params=params, return_type="otherUser")
def user_search(
self,
query: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
archived: bool = False,
emailOnly: bool = False,
) -> UserSearchResultCollection:
"""Searches for a user on the server, by name or email. The search query must be at least
3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
cursor {Optional[str]} --
archived {bool} --
emailOnly {bool} --
Returns:
ResourceCollection[LimitedUser] -- User objects that match the search query
"""
QUERY = gql(
"""
query UserSearch($query: String!, $limit: Int!, $cursor: String, $archived: Boolean, $emailOnly: Boolean) {
data:userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived, emailOnly: $emailOnly) {
cursor
items {
id
name
bio
company
avatar
verified
role
}
}
}
"""
)
variables = {
"query": query,
"limit": limit,
"cursor": cursor,
"archived": archived,
"emailOnly": emailOnly,
}
return self.make_request_and_parse_response(
DataResponse[UserSearchResultCollection], QUERY, variables
).data
@deprecated(reason="Use user_search instead", version=FE1_DEPRECATION_VERSION)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
@@ -140,13 +75,12 @@ class OtherUserResource(ResourceBase):
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
role
id
name
bio
company
avatar
verified
}
}
}
@@ -158,7 +92,6 @@ class OtherUserResource(ResourceBase):
query=query, params=params, return_type=["userSearch", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
user_id: str,
@@ -1,7 +1,6 @@
import re
from typing import Any, Dict, List, Tuple
import requests
from gql import gql
from specklepy.core.api.models import ServerInfo
@@ -11,7 +10,7 @@ from specklepy.logging.exceptions import GraphQLException
NAME = "server"
class ServerResource(ResourceBase):
class Resource(ResourceBase):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
@@ -57,21 +56,9 @@ class ServerResource(ResourceBase):
"""
)
server_info = self.make_request(
return self.make_request(
query=query, return_type="serverInfo", schema=ServerInfo
)
if isinstance(server_info, ServerInfo) and isinstance(
server_info.canonicalUrl, str
):
r = requests.get(
server_info.canonicalUrl, headers={"User-Agent": "specklepy SDK"}
)
if "x-speckle-frontend-2" in r.headers:
server_info.frontend2 = True
else:
server_info.frontend2 = False
return server_info
def version(self) -> Tuple[Any, ...]:
"""Get the server version
@@ -4,15 +4,7 @@ from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Stream,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, Stream
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
@@ -20,10 +12,7 @@ NAME = "stream"
class Resource(ResourceBase):
"""
API Access class for streams
Stream resource is deprecated, please use project resource instead
"""
"""API Access class for streams"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
@@ -36,7 +25,6 @@ class Resource(ResourceBase):
self.schema = Stream
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream:
"""Get the specified stream from the server
@@ -98,7 +86,6 @@ class Resource(ResourceBase):
return self.make_request(query=query, params=params, return_type="stream")
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_limit: int = 10) -> List[Stream]:
"""Get a list of the user's streams
@@ -152,7 +139,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["user", "streams", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
name: str = "Anonymous Python Stream",
@@ -177,8 +163,7 @@ class Resource(ResourceBase):
}
"""
)
if len(name) < 3 and len(name) != 0:
return SpeckleException(message="Stream Name must be at least 3 characters")
params = {
"stream": {"name": name, "description": description, "isPublic": is_public}
}
@@ -187,7 +172,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamCreate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
id: str,
@@ -228,7 +212,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, id: str) -> bool:
"""Delete a stream given its id
@@ -252,7 +235,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamDelete", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def search(
self,
search_query: str,
@@ -332,7 +314,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["streams", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream.
@@ -366,7 +347,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["streamFavorite"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(
self, stream_id: str
) -> List[PendingStreamCollaborator]:
@@ -394,27 +374,19 @@ class Resource(ResourceBase):
inviteId
streamId
streamName
projectName
projectId
title
role
invitedBy{
id
name
bio
company
avatar
verified
role
}
user {
id
name
bio
company
avatar
verified
role
}
}
}
@@ -430,7 +402,6 @@ class Resource(ResourceBase):
schema=PendingStreamCollaborator,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite(
self,
stream_id: str,
@@ -487,7 +458,6 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_batch(
self,
stream_id: str,
@@ -534,10 +504,11 @@ class Resource(ResourceBase):
user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id}
for user_id in (user_ids if user_ids is not None else [])
for user_id in (user_ids if user_ids is not None else [])
if user_id is not None
]
params = {"input": [*email_invites, *user_invites]}
return self.make_request(
@@ -547,7 +518,6 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite
@@ -579,7 +549,6 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite
@@ -617,7 +586,6 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream
@@ -664,7 +632,6 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
@@ -694,7 +661,6 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
stream_id: str,
@@ -762,13 +728,13 @@ class Resource(ResourceBase):
"stream_id": stream_id,
"limit": limit,
"action_type": action_type,
"before": (
before.astimezone(timezone.utc).isoformat() if before else before
),
"before": before.astimezone(timezone.utc).isoformat()
if before
else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": (
cursor.astimezone(timezone.utc).isoformat() if cursor else cursor
),
"cursor": cursor.astimezone(timezone.utc).isoformat()
if cursor
else cursor,
}
except AttributeError as e:
raise SpeckleException(
@@ -1,16 +1,11 @@
from functools import wraps
from typing import Callable, Dict, List, Optional, Union
from deprecated import deprecated
from gql import gql
from graphql import DocumentNode
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
Stream,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.resources.stream import Stream
from specklepy.logging.exceptions import SpeckleException
NAME = "subscribe"
@@ -40,7 +35,6 @@ class Resource(ResourceBase):
name=NAME,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_added(self, callback: Optional[Callable] = None):
"""Subscribes to new stream added event for your profile.
@@ -4,12 +4,9 @@ from typing import List, Optional, Union
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
User,
)
from specklepy.core.api.models import ActivityCollection, PendingStreamCollaborator, User
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "user"
-9
View File
@@ -1,9 +0,0 @@
from typing import Generic, TypeVar
from pydantic import BaseModel
T = TypeVar("T")
class DataResponse(BaseModel, Generic[T]):
data: T
+28 -124
View File
@@ -1,13 +1,11 @@
from urllib.parse import quote, unquote, urlparse
from urllib.parse import unquote, urlparse
from warnings import warn
from gql import gql
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import (
Account,
get_account_from_token,
get_accounts_for_server,
get_local_accounts,
)
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.server.server import ServerTransport
@@ -30,7 +28,7 @@ class StreamWrapper:
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
@@ -47,7 +45,6 @@ class StreamWrapper:
commit_id: str = None
object_id: str = None
branch_name: str = None
model_id: str = None
_client: SpeckleClient = None
_account: Account = None
@@ -84,86 +81,29 @@ class StreamWrapper:
" provided."
)
# check for fe2 URL
if "/projects/" in parsed.path:
use_fe2 = True
key_stream = "project"
else:
use_fe2 = False
key_stream = "stream"
while segments:
segment = segments.pop(0)
if use_fe2 is False:
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
elif segments and use_fe2 is True:
if segment.lower() == "projects":
self.stream_id = segments.pop(0)
elif segment.lower() == "models":
next_segment = segments.pop(0)
if "," in next_segment:
raise SpeckleException("Multi-model urls are not supported yet")
elif unquote(next_segment).startswith("$"):
raise SpeckleException(
"Federation model urls are not supported"
)
elif len(next_segment) == 32:
self.object_id = next_segment
else:
self.branch_name = unquote(next_segment).split("@")[0]
if "@" in unquote(next_segment):
self.commit_id = unquote(next_segment).split("@")[1]
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
if use_fe2 is True and self.branch_name is not None:
self.model_id = self.branch_name
# get branch name
query = gql(
"""
query Project($project_id: String!, $model_id: String!) {
project(id: $project_id) {
id
model(id: $model_id) {
name
}
}
}
"""
)
self._client = self.get_client()
params = {"project_id": self.stream_id, "model_id": self.model_id}
project = self._client.httpclient.execute(query, params)
try:
self.branch_name = project["project"]["model"]["name"]
except KeyError as ke:
raise SpeckleException("Project model name is not found", ke)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL"
" provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no {key_stream} id found."
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
@property
@@ -178,7 +118,14 @@ class StreamWrapper:
if self._account and self._account.token:
return self._account
self._account = next(iter(get_accounts_for_server(self.host)), None)
self._account = next(
(
a
for a in get_local_accounts()
if self.host == urlparse(a.serverInfo.url).netloc
),
None,
)
if not self._account:
self._account = get_account_from_token(token, self.server_url)
@@ -237,46 +184,3 @@ class StreamWrapper:
if not self._account or not self._account.token:
self.get_account(token)
return ServerTransport(self.stream_id, account=self._account)
def to_string(self) -> str:
"""
Constructs a URL depending on the StreamWrapper type and FE version.
"""
use_fe2 = False
key_streams = "/streams/"
key_branches = "/branches/"
if isinstance(self.branch_name, str):
value_branch = quote(self.branch_name)
if self.branch_name == "globals":
key_branches = "/"
key_commits = "/commits/"
if isinstance(self.commit_id, str) and self.branch_name == "globals":
key_commits = "/globals/"
key_objects = "/objects/"
if "/projects/" in self.stream_url:
use_fe2 = True
key_streams = "/projects/"
key_branches = "/models/"
value_branch = self.model_id
key_commits = "@"
key_objects = "/models/"
wrapper_type = self.type
if use_fe2 is False or (use_fe2 is True and not self.model_id):
base_url = f"{self.server_url}{key_streams}{self.stream_id}"
else: # fe2 is True and model_id available
base_url = f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
if wrapper_type == "object":
return f"{base_url}{key_objects}{self.object_id}"
elif wrapper_type == "commit":
return f"{base_url}{key_commits}{self.commit_id}"
elif wrapper_type == "branch":
return f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
elif wrapper_type == "stream":
return f"{self.server_url}{key_streams}{self.stream_id}"
else:
raise SpeckleException(
f"Cannot parse StreamWrapper of type '{wrapper_type}'"
)
+2 -2
View File
@@ -28,7 +28,7 @@ CONNECTOR = "Connector Action"
RECEIVE = "Receive"
SEND = "Send"
# not in use since 2.15
# not in use since 2.15
ACCOUNTS = "Get Local Accounts"
BRANCH = "Branch Action"
CLIENT = "Speckle Client"
@@ -142,7 +142,7 @@ class MetricsTracker(metaclass=Singleton):
def hash(self, value: str):
inputList = value.lower().split("://")
input = inputList[len(inputList) - 1].split("/")[0].split("?")[0]
input = inputList[len(inputList)-1].split("/")[0].split('?')[0]
return hashlib.md5(input.encode("utf-8")).hexdigest().upper()
def _send_tracking_requests(self):
+4 -3
View File
@@ -1,6 +1,5 @@
from typing import Optional
from specklepy.objects.base import Base
from specklepy.objects import Base
class CRS(Base, speckle_type="Objects.GIS.CRS"):
@@ -9,7 +8,9 @@ class CRS(Base, speckle_type="Objects.GIS.CRS"):
name: Optional[str] = None
authority_id: Optional[str] = None
wkt: Optional[str] = None
units_native: Optional[str] = None
units_native: Optional[str] = None
offset_x: Optional[float] = None
offset_y: Optional[float] = None
rotation: Optional[float] = None
+14 -16
View File
@@ -1,24 +1,22 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.GIS.layers import (
VectorLayer,
RasterLayer,
)
from specklepy.objects.GIS.geometry import (
GisPolygonGeometry,
GisPolygonElement,
GisLineElement,
GisPointElement,
GisPolygonElement,
GisPolygonGeometry,
GisRasterElement,
PolygonGeometry,
)
from specklepy.objects.GIS.layers import RasterLayer, VectorLayer
__all__ = [
"VectorLayer",
"RasterLayer",
"GisPolygonGeometry",
"PolygonGeometry",
"GisPolygonElement",
"GisLineElement",
"GisPointElement",
"GisRasterElement",
"CRS",
]
from specklepy.objects.GIS.CRS import (
CRS,
)
__all__ = ["VectorLayer", "RasterLayer",
"GisPolygonGeometry", "GisPolygonElement", "GisLineElement", "GisPointElement", "GisRasterElement",
"CRS"]
+15 -36
View File
@@ -1,26 +1,15 @@
from typing import List, Optional, Union
from specklepy.objects.base import Base
from specklepy.objects.geometry import (
Arc,
Circle,
Line,
Mesh,
Point,
Polycurve,
Polyline,
)
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 PolygonGeometry(Base, speckle_type="Objects.GIS.PolygonGeometry"):
class GisPolygonGeometry(Base, speckle_type="Objects.GIS.PolygonGeometry", detachable={"displayValue"}):
"""GIS Polygon Geometry"""
boundary: Optional[Polyline]
voids: Optional[List[Polyline]]
GisPolygonGeometry = PolygonGeometry
boundary: Optional[Union[Polyline, Arc, Line, Circle, Polycurve]] = None
voids: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]] ] = None
displayValue: Optional[List[Mesh]] = None
class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"):
"""GIS Polygon element"""
@@ -28,24 +17,19 @@ class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"):
geometry: Optional[List[GisPolygonGeometry]] = None
attributes: Optional[Base] = None
class GisLineElement(Base, speckle_type="Objects.GIS.LineElement"):
"""GIS Polyline element"""
geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None
attributes: Optional[Base] = None
geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None,
attributes: Optional[Base] = None,
class GisPointElement(Base, speckle_type="Objects.GIS.PointElement"):
"""GIS Point element"""
geometry: Optional[List[Point]] = None
attributes: Optional[Base] = None
geometry: Optional[List[Point]] = None,
attributes: Optional[Base] = None,
class GisRasterElement(
Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}
):
class GisRasterElement(Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}):
"""GIS Raster element"""
band_count: Optional[int] = None
@@ -59,16 +43,11 @@ class GisRasterElement(
noDataValue: Optional[List[float]] = None
displayValue: Optional[List[Mesh]] = None
class GisTopography(
GisRasterElement,
speckle_type="Objects.GIS.GisTopography",
detachable={"displayValue"},
):
class GisTopography(GisRasterElement, speckle_type="Objects.GIS.GisTopography", detachable={"displayValue"}):
"""GIS Raster element with 3d Topography representation"""
class GisNonGeometryElement(Base, speckle_type="Objects.GIS.NonGeometryElement"):
"""GIS Table feature"""
attributes: Optional[Base] = None
+24 -83
View File
@@ -1,26 +1,23 @@
from typing import Any, Dict, List, Optional, Union
from deprecated import deprecated
from typing import Any, Dict, List, Optional
from specklepy.objects.base import Base
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.other import Collection
from specklepy.objects.GIS.CRS import CRS
from deprecated import deprecated
@deprecated(version="2.15", reason="Use VectorLayer or RasterLayer instead")
class Layer(Base, detachable={"features"}):
"""A GIS Layer"""
def __init__(
self,
name: Optional[str] = None,
crs: Optional[CRS] = None,
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,
renderer: Optional[dict[str, Any]] = None,
**kwargs
) -> None:
super().__init__(**kwargs)
self.name = name
@@ -29,27 +26,25 @@ class Layer(Base, detachable={"features"}):
self.type = layerType
self.features = features or []
self.geomType = geomType
self.renderer = renderer or {}
self.renderer = renderer or {}
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class VectorLayer(
Collection,
detachable={"elements"},
speckle_type="VectorLayer",
serialize_ignore={"features"},
):
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.VectorLayer",
serialize_ignore={"features"}):
"""GIS Vector Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
name: Optional[str]=None
crs: Optional[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
collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
@@ -59,25 +54,24 @@ class VectorLayer(
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class RasterLayer(
Collection,
detachable={"elements"},
speckle_type="RasterLayer",
serialize_ignore={"features"},
):
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.RasterLayer",
serialize_ignore={"features"}):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
crs: Optional[CRS]=None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]] = None
rasterCrs: Optional[CRS]=None
elements: Optional[List[Base]] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
@@ -87,56 +81,3 @@ class RasterLayer(
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class VectorLayer( # noqa: F811
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.VectorLayer",
serialize_ignore={"features"},
):
"""GIS Vector Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
elements: Optional[List[Base]] = None
attributes: Optional[Base] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class RasterLayer( # noqa: F811
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.RasterLayer",
serialize_ignore={"features"},
):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]] = None
elements: Optional[List[Base]] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
+2 -19
View File
@@ -1,23 +1,6 @@
"""Builtin Speckle object kit."""
from specklepy.objects import (
GIS,
encoding,
geometry,
other,
primitive,
structural,
units,
)
from specklepy.objects import encoding, geometry, other, primitive, structural, units
from specklepy.objects.base import Base
__all__ = [
"Base",
"encoding",
"geometry",
"other",
"units",
"structural",
"primitive",
"GIS",
]
__all__ = ["Base", "encoding", "geometry", "other", "units", "structural", "primitive"]
+2 -3
View File
@@ -19,7 +19,7 @@ from warnings import warn
from stringcase import pascalcase
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.units import Units
from specklepy.objects.units import Units, get_units_from_string
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
@@ -188,8 +188,7 @@ class _RegisteringBase:
cls._detachable = cls._detachable.union(detachable)
if serialize_ignore:
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
# we know, that the super here is object, that takes no args on init subclass
return super().__init_subclass__()
super().__init_subclass__(**kwargs)
# T = TypeVar("T")
+14 -14
View File
@@ -40,9 +40,9 @@ class Point(Base, speckle_type=GEOMETRY + "Point"):
class Pointcloud(
Base,
Base,
speckle_type=GEOMETRY + "Pointcloud",
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
):
points: Optional[List[float]] = None
colors: Optional[List[int]] = None
@@ -303,15 +303,15 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
class SpiralType(Enum):
Biquadratic = 0
BiquadraticParabola = 1
Bloss = 2
Clothoid = 3
Cosine = 4
Cubic = 5
CubicParabola = 6
Radioid = 7
Sinusoid = 8
Biquadratic = (0,)
BiquadraticParabola = (1,)
Bloss = (2,)
Clothoid = (3,)
Cosine = (4,)
Cubic = (5,)
CubicParabola = (6,)
Radioid = (7,)
Sinusoid = (8,)
Unknown = 9
@@ -319,7 +319,7 @@ class Spiral(Base, speckle_type=GEOMETRY + "Spiral", detachable={"displayValue"}
startPoint: Optional[Point] = None
endPoint: Optional[Point]
plane: Optional[Plane]
turns: Optional[float]
turns: Optional[int]
pitchAxis: Optional[Vector] = Vector()
pitch: float = 0
spiralType: Optional[SpiralType] = None
@@ -898,7 +898,7 @@ class Brep(
def VerticesValue(self) -> List[Point]:
if self.Vertices is None:
return None
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
encoded_unit = get_encoding_from_units(self.Vertices[0]._units)
values = [encoded_unit]
for vertex in self.Vertices:
values.extend(vertex.to_list())
@@ -913,7 +913,7 @@ class Brep(
for i in range(0, len(value), 3):
vertex = Point.from_list(value[i : i + 3])
vertex.units = units
vertex._units = units
vertices.append(vertex)
self.Vertices = vertices
@@ -1,25 +1,24 @@
from abc import ABC, abstractmethod
from typing import Any, Collection, Dict, Generic, Iterable, Optional, Tuple, TypeVar
from dataclasses import dataclass
from typing import Any, Collection, Dict, Generic, Iterable, List, Optional, Tuple, TypeVar
from attrs import define
from specklepy.objects.base import Base
ROOT: str = "__Root"
T = TypeVar("T")
T = TypeVar('T')
PARENT_INFO = Tuple[Optional[str], str]
@define(slots=True)
class CommitObjectBuilder(ABC, Generic[T]):
converted: Dict[str, Base]
_parent_infos: Dict[str, Collection[PARENT_INFO]]
def __init__(self) -> None:
self.converted = {}
self._parent_infos = {}
@abstractmethod
def include_object(self, conversion_result: Base, native_object: T) -> None:
pass
@@ -27,17 +26,14 @@ class CommitObjectBuilder(ABC, Generic[T]):
def build_commit_object(self, root_commit_object: Base) -> None:
self.apply_relationships(self.converted.values(), root_commit_object)
def set_relationship(
self, app_id: Optional[str], *parent_info: PARENT_INFO
) -> None:
def set_relationship(self, app_id: Optional[str], *parent_info : PARENT_INFO) -> None:
if not app_id:
return
self._parent_infos[app_id] = parent_info
def apply_relationships(
self, to_add: Iterable[Base], root_commit_object: Base
) -> None:
def apply_relationships(self, to_add: Iterable[Base], root_commit_object: Base) -> None:
for c in to_add:
try:
self.apply_relationship(c, root_commit_object)
@@ -45,25 +41,20 @@ class CommitObjectBuilder(ABC, Generic[T]):
print(f"Failed to add object {type(c)} to commit object: {ex}")
def apply_relationship(self, current: Base, root_commit_object: Base):
if not current.applicationId:
raise Exception("Expected applicationId to have been set")
if not current.applicationId: raise Exception(f"Expected applicationId to have been set")
parents = self._parent_infos[current.applicationId]
for parent_id, prop_name in parents:
if not parent_id:
continue
for (parent_id, prop_name) in parents:
if not parent_id: continue
parent: Optional[Base]
if parent_id == ROOT:
parent = root_commit_object
else:
parent = (
self.converted[parent_id] if parent_id in self.converted else None
)
if not parent:
continue
parent = self.converted[parent_id] if parent_id in self.converted else None
if not parent: continue
try:
elements = get_detached_prop(parent, prop_name)
@@ -75,26 +66,18 @@ class CommitObjectBuilder(ABC, Generic[T]):
return
except Exception as ex:
# A parent was found, but it was invalid (Likely because of a type mismatch on a `elements` property)
print(
f"Failed to add object {type(current)} to a converted parent; {ex}"
)
raise Exception(
f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!"
)
print(f"Failed to add object {type(current)} to a converted parent; {ex}")
raise Exception(f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!")
def get_detached_prop(speckle_object: Base, prop_name: str) -> Optional[Any]:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
return getattr(speckle_object, detached_prop_name, None)
def set_detached_prop(
speckle_object: Base, prop_name: str, value: Optional[Any]
) -> None:
def set_detached_prop(speckle_object: Base, prop_name: str, value: Optional[Any]) -> None:
detached_prop_name = get_detached_prop_name(speckle_object, prop_name)
setattr(speckle_object, detached_prop_name, value)
def get_detached_prop_name(speckle_object: Base, prop_name: str) -> str:
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
return prop_name if hasattr(speckle_object, prop_name) else f"@{prop_name}"
@@ -3,7 +3,7 @@ from typing import Any, Callable, Collection, Iterable, Iterator, List, Optional
from attrs import define
from typing_extensions import Protocol, final
from specklepy.objects.base import Base
from specklepy.objects import Base
class ITraversalRule(Protocol):
@@ -41,6 +41,7 @@ class TraversalContext:
@final
@define(slots=True, frozen=True)
class GraphTraversal:
_rules: List[ITraversalRule]
def traverse(self, root: Base) -> Iterator[TraversalContext]:
@@ -57,15 +58,15 @@ class GraphTraversal:
members_to_traverse = active_rule.get_members_to_traverse(current)
for child_prop in members_to_traverse:
try:
if child_prop in {"speckle_type", "units", "applicationId"}:
continue # debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
if child_prop in {"speckle_type", "units", "applicationId"}: continue #debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
if getattr(current, child_prop, None):
value = current[child_prop]
self._traverse_member_to_stack(stack, value, child_prop, head)
except KeyError:
self._traverse_member_to_stack(
stack, value, child_prop, head
)
except KeyError as ex:
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
pass
@staticmethod
def _traverse_member_to_stack(
stack: List[TraversalContext],
@@ -77,14 +78,10 @@ class GraphTraversal:
stack.append(TraversalContext(value, member_name, parent))
elif isinstance(value, list):
for obj in value:
GraphTraversal._traverse_member_to_stack(
stack, obj, member_name, parent
)
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
elif isinstance(value, dict):
for obj in value.values():
GraphTraversal._traverse_member_to_stack(
stack, obj, member_name, parent
)
GraphTraversal._traverse_member_to_stack(stack, obj, member_name, parent)
@staticmethod
def traverse_member(value: Optional[Any]) -> Iterator[Base]:
@@ -99,6 +96,7 @@ class GraphTraversal:
for o in GraphTraversal.traverse_member(obj):
yield o
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
return self._get_active_rule(o) or _default_rule
@@ -122,4 +120,4 @@ class TraversalRule:
for condition in self._conditions:
if condition(o):
return True
return False
return False
+10 -23
View File
@@ -1,8 +1,7 @@
from typing import Any, List, Optional
from deprecated import deprecated
from specklepy.objects.geometry import Plane, Point, Polyline, Vector
from specklepy.objects.geometry import Point, Vector
from .base import Base
@@ -72,19 +71,6 @@ class DisplayStyle(Base, speckle_type=OTHER + "DisplayStyle"):
lineweight: float = 0
class Text(Base, speckle_type=OTHER + "Text"):
"""
Text object to render it on viewer.
"""
plane: Plane
value: str
height: float
rotation: float
displayValue: Optional[List[Polyline]] = None
richText: Optional[str] = None
class Transform(
Base,
speckle_type=OTHER + "Transform",
@@ -261,7 +247,9 @@ class BlockDefinition(
geometry: Optional[List[Base]] = None
class Instance(Base, speckle_type=OTHER + "Instance", detachable={"definition"}):
class Instance(
Base, speckle_type=OTHER + "Instance", detachable={"definition"}
):
transform: Optional[Transform] = None
definition: Optional[Base] = None
@@ -280,17 +268,17 @@ class BlockInstance(
def blockDefinition(self, value: Optional[BlockDefinition]) -> None:
self.definition = value
class RevitInstance(Instance, speckle_type=OTHER_REVIT + "RevitInstance"):
level: Optional[Base] = None
level: Optional[Base] = None
facingFlipped: bool
handFlipped: bool
parameters: Optional[Base] = None
parameters: Optional[Base] = None
elementId: Optional[str]
# TODO: prob move this into a built elements module, but just trialling this for now
class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter"):
class RevitParameter(
Base, speckle_type="Objects.BuiltElements.Revit.Parameter"
):
name: Optional[str] = None
value: Any = None
applicationUnitType: Optional[str] = None # eg UnitType UT_Length
@@ -302,10 +290,9 @@ class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter")
isReadOnly: bool = False
isTypeParameter: bool = False
class Collection(
Base, speckle_type="Speckle.Core.Models.Collection", detachable={"elements"}
):
name: Optional[str] = None
collectionType: Optional[str] = None
elements: Optional[List[Base]] = None
elements: Optional[List[Base]] = None
+4 -1
View File
@@ -6,7 +6,10 @@ from specklepy.objects.structural.analysis import (
ModelSettings,
ModelUnits,
)
from specklepy.objects.structural.axis import Axis, AxisType
from specklepy.objects.structural.axis import (
AxisType,
Axis
)
from specklepy.objects.structural.geometry import (
Element1D,
Element2D,
+1 -1
View File
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Optional
from enum import Enum
from specklepy.objects.base import Base
from specklepy.objects.geometry import Plane
+2 -4
View File
@@ -106,9 +106,7 @@ def get_encoding_from_units(unit: Union[Units, str, None]):
def get_scale_factor_from_string(fromUnits: str, toUnits: str) -> float:
"""Returns a scalar to convert distance values from one unit system to another"""
return get_scale_factor(
get_units_from_string(fromUnits), get_units_from_string(toUnits)
)
return get_scale_factor(get_units_from_string(fromUnits), get_units_from_string(toUnits))
def get_scale_factor(fromUnits: Units, toUnits: Units) -> float:
@@ -121,4 +119,4 @@ def get_scale_factor_to_meters(fromUnits: Units) -> float:
if fromUnits not in UNIT_SCALE:
raise ValueError(f"Invalid units provided: {fromUnits}")
return UNIT_SCALE[fromUnits]
return UNIT_SCALE[fromUnits]
+1 -1
View File
@@ -1,4 +1,4 @@
from typing import Dict, List
from typing import Any, Dict, List
from specklepy.transports.abstract_transport import AbstractTransport
@@ -137,7 +137,7 @@ class BatchSender(object):
raise SpeckleException(
message=(
"Could not save the object to the server - status code"
f" {r.status_code} ({r.text[:1000]})"
f" {r.status_code} ({r.text[:1000]})"
)
)
except json.JSONDecodeError as error:
+9 -13
View File
@@ -1,5 +1,5 @@
import json
from typing import Dict, List, Optional
from typing import Any, Dict, List, Optional
from warnings import warn
import requests
@@ -73,7 +73,7 @@ class ServerTransport(AbstractTransport):
warn(
SpeckleWarning(
"Unauthenticated Speckle Client provided to Server Transport"
f" for {url}. Receiving from private streams will fail."
f" for {self.url}. Receiving from private streams will fail."
)
)
else:
@@ -84,18 +84,14 @@ class ServerTransport(AbstractTransport):
self.stream_id = stream_id
self.url = url
self.session = requests.Session()
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
if self.account is not None:
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
self.session.headers.update(
{
"Authorization": f"Bearer {self.account.token}",
"Accept": "text/plain",
}
)
self.session = requests.Session()
self.session.headers.update(
{"Authorization": f"Bearer {self.account.token}", "Accept": "text/plain"}
)
@property
def name(self) -> str:
+1 -1
View File
@@ -1,7 +1,7 @@
import os
import sqlite3
from contextlib import closing
from typing import Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging.exceptions import SpeckleException
@@ -1,44 +0,0 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.models import ResourceCollection, User
@pytest.mark.run()
class TestActiveUserResource:
def test_active_user_get(self, client: SpeckleClient):
res = client.active_user.get()
assert isinstance(res, User)
def test_active_user_update(self, client: SpeckleClient):
NEW_NAME = "Ron"
NEW_BIO = "Now I have a bio, isn't that nice!"
NEW_COMPANY = "Limited Cooperation Organization Inc"
input = UserUpdateInput(name=NEW_NAME, bio=NEW_BIO, company=NEW_COMPANY)
res = client.active_user.update(input=input)
assert isinstance(res, User)
assert res.name == NEW_NAME
assert res.bio == NEW_BIO
assert res.company == NEW_COMPANY
def test_active_user_get_projects(self, client: SpeckleClient):
existing = client.active_user.get_projects()
p1 = client.project.create(
ProjectCreateInput(name="Project 1", description=None, visibility=None)
)
p2 = client.project.create(
ProjectCreateInput(name="Project 2", description=None, visibility=None)
)
res = client.active_user.get_projects()
assert isinstance(res, ResourceCollection)
assert len(res.items) == len(existing.items) + 2
assert any(project.id == p1.id for project in res.items)
assert any(project.id == p2.id for project in res.items)
@@ -1,122 +0,0 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
from specklepy.core.api.models.current import (
Model,
Project,
ProjectWithModels,
ResourceCollection,
)
from specklepy.logging.exceptions import GraphQLException
@pytest.mark.run()
class TestModelResource:
@pytest.fixture()
def test_project(self, client: SpeckleClient) -> Project:
project = client.project.create(
ProjectCreateInput(name="Test project", description="", visibility=None)
)
return project
@pytest.fixture()
def test_model(self, client: SpeckleClient, test_project: Project) -> Model:
model = client.model.create(
CreateModelInput(
name="Test Model", description="", projectId=test_project.id
)
)
return model
@pytest.mark.parametrize(
"name, description",
[
("My Model", "My model description"),
("my/nested/model", None),
],
)
def test_model_create(
self, client: SpeckleClient, test_project: Project, name: str, description: str
):
input = CreateModelInput(
name=name, description=description, projectId=test_project.id
)
result = client.model.create(input)
assert isinstance(result, Model)
assert result.name.lower() == name.lower()
assert result.description == description
def test_model_get(
self, client: SpeckleClient, test_model: Model, test_project: Project
):
result = client.model.get(test_model.id, test_project.id)
assert isinstance(result, Model)
assert result.id == test_model.id
assert result.name == test_model.name
assert result.description == test_model.description
assert result.createdAt == test_model.createdAt
assert result.updatedAt == test_model.updatedAt
def test_get_models(
self, client: SpeckleClient, test_project: Project, test_model: Model
):
result = client.model.get_models(test_project.id)
assert isinstance(result, ResourceCollection)
assert len(result.items) == 1
assert result.totalCount == 1
assert result.items[0].id == test_model.id
def test_project_get_models(
self, client: SpeckleClient, test_project: Project, test_model: Model
):
result = client.project.get_with_models(test_project.id)
assert isinstance(result, ProjectWithModels)
assert result.id == test_project.id
assert len(result.models.items) == 1
assert result.models.totalCount == 1
assert result.models.items[0].id == test_model.id
def test_model_update(
self, client: SpeckleClient, test_model: Model, test_project: Project
):
new_name = "MY new name"
new_description = "MY new desc"
update_data = UpdateModelInput(
id=test_model.id,
name=new_name,
description=new_description,
projectId=test_project.id,
)
updated_model = client.model.update(update_data)
assert isinstance(updated_model, Model)
assert updated_model.id == test_model.id
assert updated_model.name.lower() == new_name.lower()
assert updated_model.description == new_description
assert updated_model.updatedAt >= test_model.updatedAt
def test_model_delete(
self, client: SpeckleClient, test_model: Model, test_project: Project
):
delete_data = DeleteModelInput(id=test_model.id, projectId=test_project.id)
response = client.model.delete(delete_data)
assert response is True
with pytest.raises(GraphQLException):
client.model.get(test_model.id, test_project.id)
with pytest.raises(GraphQLException):
client.model.delete(delete_data)
@@ -1,32 +0,0 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.models import User
@pytest.mark.run()
class TestOtherUserResource:
@pytest.fixture(scope="class")
def test_data(self, second_client: SpeckleClient) -> User:
user_info = second_client.active_user.get()
assert user_info
return user_info
def test_other_user_get(self, client: SpeckleClient, test_data: User):
res = client.other_user.get(test_data.id)
assert res is not None
assert res.name == test_data.name
def test_other_user_get_non_existent_user(self, client: SpeckleClient):
result = client.other_user.get("AnIdThatDoesntExist")
assert result is None
def test_user_search(self, client: SpeckleClient, test_data: User):
assert test_data.email
res = client.other_user.user_search(test_data.email, limit=25)
assert len(res.items) == 1
assert res.items[0].id == test_data.id
def test_user_search_non_existent_user(self, client: SpeckleClient):
res = client.other_user.user_search("idontexist@example.com", limit=25)
assert len(res.items) == 0
@@ -1,176 +0,0 @@
from typing import Optional
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectInviteCreateInput,
ProjectInviteUseInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.models import (
LimitedUser,
PendingStreamCollaborator,
Project,
ProjectWithTeam,
)
@pytest.mark.run()
class TestProjectInviteResource:
@pytest.fixture
def project(self, client: SpeckleClient):
return client.project.create(
ProjectCreateInput(name="test", description=None, visibility=None)
)
@pytest.fixture
def created_invite(
self, client: SpeckleClient, second_client: SpeckleClient, project: Project
):
input = ProjectInviteCreateInput(
email=second_client.account.userInfo.email,
role=None,
serverRole=None,
userId=None,
)
res = client.project_invite.create(project.id, input)
invites = second_client.active_user.get_project_invites()
return next(i for i in invites if i.projectId == res.id)
def test_project_invite_create_by_email(
self, client: SpeckleClient, second_client: SpeckleClient, project: Project
):
input = ProjectInviteCreateInput(
email=second_client.account.userInfo.email,
role=None,
serverRole=None,
userId=None,
)
res = client.project_invite.create(project.id, input)
invites = second_client.active_user.get_project_invites()
invite = next(i for i in invites if i.projectId == res.id)
assert isinstance(res, ProjectWithTeam)
assert res.id == project.id
assert len(res.invitedTeam) == 1
assert isinstance(invite.user, LimitedUser)
assert invite.user.id == second_client.account.userInfo.id
assert invite.token
def test_project_invite_create_by_user_id(
self, client: SpeckleClient, second_client: SpeckleClient, project: Project
):
input = ProjectInviteCreateInput(
email=None,
role=None,
serverRole=None,
userId=second_client.account.userInfo.id,
)
res = client.project_invite.create(project.id, input)
assert isinstance(res, ProjectWithTeam)
assert res.id == project.id
assert len(res.invitedTeam) == 1
invited_team_member = res.invitedTeam[0].user
assert isinstance(invited_team_member, LimitedUser)
assert invited_team_member.id == second_client.account.userInfo.id
def test_project_invite_get(
self,
second_client: SpeckleClient,
project: Project,
created_invite: PendingStreamCollaborator,
):
collaborator = second_client.project_invite.get(
project.id, created_invite.token
)
assert isinstance(collaborator, PendingStreamCollaborator)
assert collaborator.inviteId == created_invite.inviteId
assert isinstance(collaborator.user, LimitedUser)
assert isinstance(created_invite.user, LimitedUser)
assert collaborator.user.id == created_invite.user.id
def test_project_invite_get_non_existing(
self, second_client: SpeckleClient, project: Project
):
collaborator = second_client.project_invite.get(
project.id, "this is not a real token"
)
assert collaborator is None
def test_project_invite_use_member_added(
self,
client: SpeckleClient,
second_client: SpeckleClient,
project: Project,
created_invite: PendingStreamCollaborator,
):
assert created_invite.token
input = ProjectInviteUseInput(
accept=True, projectId=created_invite.projectId, token=created_invite.token
)
res = second_client.project_invite.use(input)
assert res is True
project = client.project.get_with_team(project.id)
assert isinstance(project, ProjectWithTeam)
team_members = [c.user.id for c in project.team]
expected_team_members = [
client.account.userInfo.id,
second_client.account.userInfo.id,
]
assert set(team_members) == set(expected_team_members)
def test_project_invite_cancel_member_not_added(
self, client: SpeckleClient, created_invite: PendingStreamCollaborator
):
res = client.project_invite.cancel(
created_invite.projectId, created_invite.inviteId
)
assert isinstance(res, ProjectWithTeam)
assert len(res.invitedTeam) == 0
@pytest.mark.parametrize(
"new_role", ["stream:owner", "stream:contributor", "stream:reviewer", None]
)
def test_project_update_role(
self,
client: SpeckleClient,
second_client: SpeckleClient,
project: Project,
new_role: Optional[str],
created_invite: PendingStreamCollaborator,
):
assert created_invite.token
input = ProjectInviteUseInput(
accept=True, projectId=created_invite.projectId, token=created_invite.token
)
res = second_client.project_invite.use(input)
invitee_id = second_client.account.userInfo.id
assert invitee_id
input = ProjectUpdateRoleInput(
userId=invitee_id,
projectId=project.id,
role=new_role,
)
res = client.project.update_role(input)
assert isinstance(res, ProjectWithTeam)
final_project = second_client.project.get(project.id)
assert isinstance(res, Project)
assert final_project.role == new_role
@@ -1,93 +0,0 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectUpdateInput,
)
from specklepy.core.api.models import Project
from specklepy.logging.exceptions import GraphQLException
@pytest.mark.run()
class TestProjectResource:
@pytest.fixture()
def test_project(self, client: SpeckleClient) -> Project:
project = client.project.create(
ProjectCreateInput(
name="test project123",
description="desc",
visibility=ProjectVisibility.PRIVATE,
)
)
return project
@pytest.mark.parametrize(
"name, description, visibility",
[
("Very private project", "My secret project", ProjectVisibility.PRIVATE),
("Very public project", None, ProjectVisibility.PUBLIC),
],
)
def test_project_create(
self,
client: SpeckleClient,
name: str,
description: str,
visibility: ProjectVisibility,
):
input = ProjectCreateInput(
name=name,
description=description,
visibility=visibility,
)
result = client.project.create(input)
assert isinstance(result, Project)
assert result.id is not None
assert result.name == name
assert result.description == (description or "")
assert result.visibility == visibility
def test_project_get(self, client: SpeckleClient, test_project: Project):
result = client.project.get(test_project.id)
assert isinstance(result, Project)
assert result.id == test_project.id
assert result.name == test_project.name
assert result.description == test_project.description
assert result.visibility == test_project.visibility
assert result.createdAt == test_project.createdAt
def test_project_update(self, client: SpeckleClient, test_project: Project):
new_name = "MY new name"
new_description = "MY new desc"
new_visibility = ProjectVisibility.PUBLIC
update_data = ProjectUpdateInput(
id=test_project.id,
name=new_name,
description=new_description,
visibility=new_visibility,
)
updated_project = client.project.update(update_data)
assert isinstance(updated_project, Project)
assert updated_project.id == test_project.id
assert updated_project.name == new_name
assert updated_project.description == new_description
assert updated_project.visibility == new_visibility
def test_project_delete(self, client: SpeckleClient):
"""Test deleting a project."""
project_to_delete = client.project.create(
ProjectCreateInput(name="Delete me", description=None, visibility=None)
)
response = client.project.delete(project_to_delete.id)
assert response is True
with pytest.raises(GraphQLException):
client.project.get(project_to_delete.id)
@@ -1,185 +0,0 @@
import asyncio
from typing import Dict, Optional
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.enums import (
ProjectModelsUpdatedMessageType,
ProjectUpdatedMessageType,
ProjectVersionsUpdatedMessageType,
UserProjectsUpdatedMessageType,
)
from specklepy.core.api.inputs.model_inputs import CreateModelInput
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectUpdateInput,
)
from specklepy.core.api.models import (
Model,
Project,
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
Version,
)
from tests.integration.conftest import create_client, create_version
WAIT_PERIOD = 0.4 # time in seconds
@pytest.mark.run()
class TestSubscriptionResource:
@pytest.fixture
def subscription_client(
self, host: str, user_dict: Dict[str, str]
) -> SpeckleClient:
return create_client(host, user_dict["token"])
@pytest.fixture
def test_project(self, subscription_client: SpeckleClient) -> Project:
project = subscription_client.project.create(
ProjectCreateInput(name="Test project", description="", visibility=None)
)
return project
@pytest.fixture
def test_model(
self, subscription_client: SpeckleClient, test_project: Project
) -> Model:
model1 = subscription_client.model.create(
CreateModelInput(
name="Test Model 1", description="", projectId=test_project.id
)
)
return model1
@pytest.mark.asyncio
async def test_user_projects_updated(
self,
subscription_client: SpeckleClient,
) -> None:
message: Optional[UserProjectsUpdatedMessage] = None
task = None
def callback(d: UserProjectsUpdatedMessage):
nonlocal message
message = d
task = asyncio.create_task(
subscription_client.subscription.user_projects_updated(callback)
)
await asyncio.sleep(WAIT_PERIOD) # Give time to subscription to be setup
input = ProjectCreateInput(name=None, description=None, visibility=None)
created = subscription_client.project.create(input)
await asyncio.sleep(WAIT_PERIOD) # Give time for subscription to be triggered
assert isinstance(message, UserProjectsUpdatedMessage)
assert message.id == created.id
assert message.type == UserProjectsUpdatedMessageType.ADDED
assert isinstance(message.project, Project)
task.cancel()
await task
@pytest.mark.asyncio
async def test_project_models_updated(
self, subscription_client: SpeckleClient, test_project: Project
) -> None:
message: Optional[ProjectModelsUpdatedMessage] = None
task = None
def callback(d: ProjectModelsUpdatedMessage):
nonlocal message
message = d
task = asyncio.create_task(
subscription_client.subscription.project_models_updated(
callback, test_project.id
)
)
await asyncio.sleep(WAIT_PERIOD) # Give time to subscription to be setup
input = CreateModelInput(
name="my model", description="myDescription", projectId=test_project.id
)
created = subscription_client.model.create(input)
await asyncio.sleep(WAIT_PERIOD) # Give time for subscription to be triggered
assert isinstance(message, ProjectModelsUpdatedMessage)
assert message.id == created.id
assert message.type == ProjectModelsUpdatedMessageType.CREATED
assert isinstance(message.model, Model)
task.cancel()
await task
@pytest.mark.asyncio
async def test_project_updated(
self, subscription_client: SpeckleClient, test_project: Project
) -> None:
message: Optional[ProjectUpdatedMessage] = None
task = None
def callback(d: ProjectUpdatedMessage):
nonlocal message
message = d
task = asyncio.create_task(
subscription_client.subscription.project_updated(callback, test_project.id)
)
await asyncio.sleep(WAIT_PERIOD) # Give time to subscription to be setup
input = ProjectUpdateInput(id=test_project.id, name="This is my new name")
created = subscription_client.project.update(input)
await asyncio.sleep(WAIT_PERIOD) # Give time for subscription to be triggered
assert isinstance(message, ProjectUpdatedMessage)
assert message.id == created.id
assert message.type == ProjectUpdatedMessageType.UPDATED
assert isinstance(message.project, Project)
task.cancel()
await task
@pytest.mark.asyncio
async def test_project_versions_updated(
self,
subscription_client: SpeckleClient,
test_project: Project,
test_model: Model,
) -> None:
message: Optional[ProjectVersionsUpdatedMessage] = None
task = None
def callback(d: ProjectVersionsUpdatedMessage):
nonlocal message
message = d
task = asyncio.create_task(
subscription_client.subscription.project_versions_updated(
callback, test_project.id
)
)
await asyncio.sleep(WAIT_PERIOD) # Give time to subscription to be setup
created = create_version(subscription_client, test_project.id, test_model.id)
await asyncio.sleep(WAIT_PERIOD) # Give time for subscription to be triggered
assert isinstance(message, ProjectVersionsUpdatedMessage)
assert message.id == created.id
assert message.type == ProjectVersionsUpdatedMessageType.CREATED
assert isinstance(message.version, Version)
task.cancel()
await task
@@ -1,157 +0,0 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.model_inputs import CreateModelInput
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
from specklepy.core.api.inputs.version_inputs import (
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
from specklepy.core.api.models import (
Model,
ModelWithVersions,
Project,
ResourceCollection,
Version,
)
from specklepy.logging.exceptions import GraphQLException
from tests.integration.conftest import create_version
@pytest.mark.run()
class TestVersionResource:
@pytest.fixture
def test_project(self, client: SpeckleClient) -> Project:
project = client.project.create(
ProjectCreateInput(name="Test project", description="", visibility=None)
)
return project
@pytest.fixture
def test_model_1(self, client: SpeckleClient, test_project: Project) -> Model:
model1 = client.model.create(
CreateModelInput(
name="Test Model 1", description="", projectId=test_project.id
)
)
return model1
@pytest.fixture
def test_model_2(self, client: SpeckleClient, test_project: Project) -> Model:
model2 = client.model.create(
CreateModelInput(
name="Test Model 2", description="", projectId=test_project.id
)
)
return model2
@pytest.fixture
def test_version(
self, client: SpeckleClient, test_project: Project, test_model_1: Model
) -> Version:
return create_version(client, test_project.id, test_model_1.id)
def test_version_get(
self, client: SpeckleClient, test_version: Version, test_project: Project
):
result = client.version.get(test_version.id, test_project.id)
assert isinstance(result, Version)
assert result.id == test_version.id
assert result.message == test_version.message
def test_versions_get(
self,
client: SpeckleClient,
test_model_1: Model,
test_project: Project,
test_version: Version,
):
result = client.version.get_versions(test_model_1.id, test_project.id)
assert isinstance(result, ResourceCollection)
assert len(result.items) == 1
assert result.totalCount == 1
assert result.items[0].id == test_version.id
def test_version_received(
self, client: SpeckleClient, test_version: Version, test_project: Project
):
input = MarkReceivedVersionInput(
versionId=test_version.id,
projectId=test_project.id,
sourceApplication="Integration test",
)
result = client.version.received(input)
assert result is True
def test_model_get_with_versions(
self,
client: SpeckleClient,
test_model_1: Model,
test_project: Project,
test_version: Version,
):
result = client.model.get_with_versions(test_model_1.id, test_project.id)
assert isinstance(result, ModelWithVersions)
assert result.id == test_model_1.id
assert len(result.versions.items) == 1
assert result.versions.totalCount == 1
assert result.versions.items[0].id == test_version.id
def test_version_update(
self, client: SpeckleClient, test_version: Version, test_project: Project
):
new_message = "MY new version message"
input = UpdateVersionInput(
versionId=test_version.id, projectId=test_project.id, message=new_message
)
updated_version = client.version.update(input)
assert isinstance(updated_version, Version)
assert updated_version.id == test_version.id
assert updated_version.message == new_message
assert updated_version.previewUrl == test_version.previewUrl
def test_version_move_to_model(
self,
client: SpeckleClient,
test_project: Project,
test_version: Version,
test_model_2: Model,
):
input = MoveVersionsInput(
targetModelName=test_model_2.name,
versionIds=[test_version.id],
projectId=test_project.id,
)
moved_model_id = client.version.move_to_model(input)
assert isinstance(moved_model_id, str)
assert moved_model_id == test_model_2.id
moved_version = client.version.get(test_version.id, test_project.id)
assert isinstance(moved_version, Version)
assert moved_version.id == test_version.id
assert moved_version.message == test_version.message
assert moved_version.previewUrl == test_version.previewUrl
def test_version_delete(
self, client: SpeckleClient, test_version: Version, test_project: Project
):
input = DeleteVersionsInput(
versionIds=[test_version.id], projectId=test_project.id
)
response = client.version.delete(input)
assert response is True
with pytest.raises(GraphQLException):
client.version.get(test_version.id, test_project.id)
with pytest.raises(GraphQLException):
client.version.delete(input)

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