Compare commits

..

5 Commits

Author SHA1 Message Date
KatKatKateryna 2f7113f0c5 linter 2024-08-04 23:25:43 +01:00
KatKatKateryna 7f38b24421 isort 2024-08-04 22:52:56 +01:00
KatKatKateryna cac4551fdf extension 2024-08-04 21:50:22 +01:00
KatKatKateryna 813bc8be4d gisfeature class 2024-08-04 21:44:29 +01:00
KatKatKateryna a45ae03235 return deprecated command 2024-08-04 21:42:28 +01:00
156 changed files with 6071 additions and 7402 deletions
+103 -10
View File
@@ -1,15 +1,108 @@
version: 2.1
# Define the jobs we want to run for this project
jobs:
build:
docker:
- image: cimg/base:2023.03
steps:
- run: echo "so long and thanks for all the fish"
orbs:
codecov: codecov/codecov@3.3.0
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
resource_class: medium
parameters:
tag:
default: "3.11"
type: string
steps:
- checkout
- run:
name: Install python
command: |
pyenv install -s << parameters.tag >>
pyenv global << parameters.tag >>
- run:
name: Startup the Speckle Server
command: docker compose -f docker-compose.yml up -d
- run:
name: Install Poetry
command: |
pip install poetry
- run:
name: Install packages
command: poetry install
- run:
name: Run tests
command: poetry run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
- store_test_results:
path: reports
- store_artifacts:
path: reports
- codecov/upload
deploy:
docker:
- image: "cimg/python:3.8"
steps:
- checkout
- run: python patch_version.py $CIRCLE_TAG
- run: poetry build
- run: poetry publish -u __token__ -p $PYPI_TOKEN
# Orchestrate our job run sequence
workflows:
build_and_test:
main:
jobs:
- build
- pre-commit:
filters:
tags:
only: /.*/
- test:
matrix:
parameters:
tag: ["3.11"]
filters:
tags:
only: /.*/
- deploy:
context: pypi
requires:
- pre-commit
- test
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
-56
View File
@@ -1,56 +0,0 @@
name: "Specklepy test and build"
on:
# pull_request:
# branches:
# - 'v3-dev'
push:
branches:
- "gergo/uvSetup"
jobs:
ci:
name: continuous-integration
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
steps:
- uses: actions/checkout@v4
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install the project
run: uv sync --all-extras --dev
- uses: actions/cache@v3
with:
path: ~/.cache/pre-commit/
key: ${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit
run: uv run pre-commit run --all-files
# - name: Run Speckle Server
# run: docker compose up -d
# - name: Run tests
# run: uv run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
# - uses: codecov/codecov-action@v5
# if: matrix.python-version == 3.13
# with:
# fail_ci_if_error: true # optional (default = false)
# files: ./reports/test-results.xml # optional
# token: ${{ secrets.CODECOV_TOKEN }}
- name: Minimize uv cache
run: uv cache prune --ci
-36
View File
@@ -1,36 +0,0 @@
# Publish a release to PyPI.
# name: 'Publish to PyPI'
# on:
# push:
# branches:
# - 'gergo/uvSetup'
# jobs:
# pypi-publish:
# name: Upload to PyPI
# runs-on: ubuntu-latest
# environment:
# name: release
# permissions:
# # For PyPI's trusted publishing.
# id-token: write
# steps:
# - name: 'Install uv'
# uses: astral-sh/setup-uv@v5
# - uses: actions/checkout@v4
# with:
# # This is necessary so that we have the tags.
# fetch-depth: 0
# - uses: mtkennerly/dunamai-action@v1
# with:
# env-var: MY_VERSION
# args: --style semver
# - run: echo $MY_VERSION
# - name: 'Build artifacts'
# run: uv build
# - name: Publish to PyPi
# run: uv publish --publish-url https://test.pypi.org/simple/
# - name: Test package install
# run: uv run --with specklepy --no-project -- python -c "import specklepy"
+17 -15
View File
@@ -1,31 +1,33 @@
repos:
- repo: local
- repo: https://github.com/charliermarsh/ruff-pre-commit
hooks:
# Run the linter.
- id: ruff
name: ruff lint
entry: uv run ruff check --force-exclude
language: system
types_or: [python, pyi]
# Run the formatter.
- id: ruff-format
name: ruff format
entry: uv run ruff format --force-exclude
language: system
types_or: [python, pyi]
rev: v0.1.6
- repo: https://github.com/commitizen-tools/commitizen
hooks:
- id: commitizen
- id: commitizen-branch
stages:
- pre-push
- push
rev: v3.13.0
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.11.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
# supported by your project here, or alternatively use
# pre-commit's default_language_version, see
# https://pre-commit.com/#top_level-default_language_version
# language_version: python3.11
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v4.5.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+5 -3
View File
@@ -4,9 +4,11 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "debugpy",
"type": "python",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
@@ -14,9 +16,9 @@
},
{
"name": "Pytest",
"type": "debugpy",
"type": "python",
"request": "launch",
"module": "pytest",
"program": "pytest",
"args": [],
"console": "integratedTerminal",
"justMyCode": true
+43 -13
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](https://img.shields.io/badge/https://-app.speckle.systems-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://app.speckle.systems) ⇒ 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
@@ -25,25 +55,25 @@ Head to the [**📚 specklepy docs**](https://speckle.guide/dev/python.html) for
### Installation
This project uses uv for dependency management, make sure you follow the official [docs](https://docs.astral.sh/uv/) to get it.
This project uses python-poetry for dependency management, make sure you follow the official [docs](https://python-poetry.org/docs/#installation) to get poetry.
To create a new virtual environment with uv run `$ uv venv` and follow the instructions on the screen to activate the virtual environment.
To bootstrap the project environment run `$ uv sync`. This will install both the package and dev dependencies.
To bootstrap the project environment run `$ poetry install`. This will create a new virtual-env for the project and install both the package and dev dependencies.
To execute any python script run `$ uv run python my_script.py`
If this is your first time using poetry and you're used to creating your venvs within the project directory, run `poetry config virtualenvs.in-project true` to configure poetry to do the same.
> Alternatively you may roll your own virtual-env with either venv, virtualenv, pyenv-virtualenv etc. Uv will play along an recognize if it is invoked from inside a virtual environment.
To execute any python script run `$ poetry run python my_script.py`
> Alternatively you may roll your own virtual-env with either venv, virtualenv, pyenv-virtualenv etc. Poetry will play along an recognize if it is invoked from inside a virtual environment.
### Style guide
All our repo wide styling linting and other rules are checked and enforced by `pre-commit`, which is included in the dev dependencies.
All our repo wide styling linting and other rules are checked and enforced by `pre-commit`, which is included in the dev dependencies.
It is recommended to set up `pre-commit` after installing the dependencies by running `$ pre-commit install`.
Commiting code that doesn't adhere to the given rules, will fail the checks in our CI system.
### Local Data Paths
It may be helpful to know where the local accounts and object cache dbs are stored. Depending on on your OS, you can find the dbs at:
- Windows: `APPDATA` or `<USER>\AppData\Roaming\Speckle`
- Linux: `$XDG_DATA_HOME` or by default `~/.local/share/Speckle`
- Mac: `~/.config/Speckle`
+57 -9
View File
@@ -6,7 +6,7 @@ services:
# Speckle Server dependencies
#######
postgres:
image: "postgres:16-alpine"
image: "postgres:14.5-alpine"
restart: always
environment:
POSTGRES_DB: speckle
@@ -52,20 +52,28 @@ services:
####
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend:latest
restart: always
ports:
- "0.0.0.0:8080:8080"
environment:
FILE_SIZE_LIMIT_MB: 100
speckle-server:
image: speckle/speckle-server:latest
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:
@@ -90,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"
@@ -104,6 +111,47 @@ services:
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
preview-service:
image: speckle/speckle-preview-service:latest
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:latest
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:latest
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
+2 -2
View File
@@ -1,8 +1,8 @@
from devtools import debug
from specklepy.api import operations
from specklepy.objects_v2.geometry import Base
from specklepy.objects_v2.units import Units
from specklepy.objects.geometry import Base
from specklepy.objects.units import Units
dct = {
"id": "1234abcd",
+31
View File
@@ -0,0 +1,31 @@
import re
import sys
def patch(tag):
print(f"Patching version: {tag}")
with open("pyproject.toml", "r") as f:
lines = f.readlines()
if "version" not in lines[2]:
raise Exception("Invalid pyproject.toml. Could not patch version.")
lines[2] = f'version = "{tag}"\n'
with open("pyproject.toml", "w") as file:
file.writelines(lines)
def main():
if len(sys.argv) < 2:
return
tag = sys.argv[1]
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
raise ValueError(f"Invalid tag provided: {tag}")
patch(tag)
if __name__ == "__main__":
main()
Generated
+1858
View File
File diff suppressed because it is too large Load Diff
+61 -57
View File
@@ -1,70 +1,74 @@
[project]
dynamic = ["version"]
[tool.poetry]
name = "specklepy"
version = "2.17.14"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = [{ name = "Speckle Systems", email = "devops@speckle.systems" }]
license = { text = "Apache-2.0" }
requires-python = ">=3.10.0, <4.0"
dependencies = [
"appdirs>=1.4.4",
"attrs>=24.3.0",
"deprecated>=1.2.15",
"gql[requests,websockets]>=3.5.0",
"httpx>=0.28.1",
"pydantic>=2.10.5",
"pydantic-settings>=2.7.1",
"stringcase>=1.2.0",
"ujson>=5.10.0",
]
[dependency-groups]
dev = [
"commitizen>=4.1.0",
"devtools>=0.12.2",
"pre-commit>=4.0.1",
"pytest>=8.3.4",
"pytest-asyncio>=0.25.2",
"pytest-cov>=6.0.0",
"pytest-ordering>=0.6",
"ruff>=0.9.2",
"types-deprecated>=1.2.15.20241117",
"types-requests>=2.32.0.20241016",
"types-ujson>=5.10.0.20240515",
]
[project.urls]
repository = "https://github.com/specklesystems/specklepy"
authors = ["Speckle Systems <devops@speckle.systems>"]
license = "Apache-2.0"
repository = "https://github.com/specklesystems/speckle-py"
documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
packages = [
{ include = "specklepy", from = "src" },
{ include = "speckle_automate", from = "src" },
]
[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
[tool.poetry.dependencies]
python = ">=3.8.0, <4.0"
pydantic = "^2.5"
appdirs = "^1.4.4"
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"
isort = "^5.7.0"
pytest = "^7.1.3"
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"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
[tool.black]
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
include = '\.pyi?$'
line-length = 88
target-version = ["py37", "py38", "py39", "py310", "py311"]
[tool.commitizen]
name = "cz_conventional_commits"
version = "2.9.2"
tag_format = "$version"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
exclude = [".venv", "**/*.yml"]
[tool.ruff.lint]
select = [
# pycodestyle
"E",
# Pyflakes
"F",
# pyupgrade
"UP",
# flake8-bugbear
"B",
# flake8-simplify
"SIM",
# isort
"I",
]
ignore = ["UP006", "UP007", "UP035"]
[tool.isort]
profile = "black"
+5 -37
View File
@@ -96,23 +96,13 @@ class AutomationContext:
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
# 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 or not commit.referencedObject:
raise ValueError(
f"""\
Could not receive specified version.
{"The commit has no referencedObject." if not commit.referencedObject else ""}
Is your environment configured correctly?
project_id: {self.automation_run_data.project_id}
model_id: {self.automation_run_data.triggers[0].payload.model_id}
version_id: {self.automation_run_data.triggers[0].payload.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
)
@@ -216,7 +206,6 @@ class AutomationContext:
query = gql(
"""
mutation AutomateFunctionRunStatusReport(
$projectId: String!
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
@@ -224,7 +213,6 @@ class AutomationContext:
$contextView: String
){
automateFunctionRunStatusReport(input: {
projectId: $projectId
functionRunId: $functionRunId
status: $status
statusMessage: $statusMessage
@@ -248,7 +236,6 @@ class AutomationContext:
object_results = None
params = {
"projectId": self.automation_run_data.project_id,
"functionRunId": self.automation_run_data.function_run_id,
"status": self.run_status.value,
"statusMessage": self._automation_result.status_message,
@@ -274,11 +261,10 @@ class AutomationContext:
if not path_obj.exists():
raise ValueError("The given file path doesn't exist")
files = {path_obj.name: path_obj.open("rb")}
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.speckle_server_url}/api/stream/"
f"{self.automation_run_data.project_id}/blob"
)
data = (
@@ -369,24 +355,6 @@ class AutomationContext:
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,
-1
View File
@@ -1,5 +1,4 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
+10 -7
View File
@@ -61,13 +61,15 @@ def _parse_input_data(
def execute_automate_function(
automate_function: AutomateFunction[T],
input_schema: type[T],
) -> None: ...
) -> None:
...
@overload
def execute_automate_function(
automate_function: AutomateFunctionWithoutInputs,
) -> None: ...
) -> None:
...
class AutomateGenerateJsonSchema(GenerateJsonSchema):
@@ -128,8 +130,7 @@ def execute_automate_function(
automate_function, # type: ignore
)
# if we've gotten this far,
# the execution should technically be completed as expected
# 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
@@ -145,14 +146,16 @@ def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunction[T],
inputs: T,
) -> AutomationContext: ...
) -> AutomationContext:
...
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunctionWithoutInputs,
) -> AutomationContext: ...
) -> AutomationContext:
...
def run_function(
@@ -191,4 +194,4 @@ def run_function(
if not automation_context.context_view:
automation_context.set_context_view()
automation_context.report_run_status()
return automation_context
return automation_context
-1
View File
@@ -69,7 +69,6 @@ class AutomationStatus(str, Enum):
class ObjectResultLevel(str, Enum):
"""Possible status message levels for object reports."""
SUCCESS = "SUCCESS"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
+2 -2
View File
@@ -1,3 +1,3 @@
# from specklepy import objects
from specklepy import objects
# __all__ = ["objects"]
__all__ = ["objects"]
+20 -60
View File
@@ -1,20 +1,13 @@
import contextlib
from deprecated import deprecated
from specklepy.api.credentials import Account
from specklepy.api.resources import (
ActiveUserResource,
ModelResource,
OtherUserResource,
ProjectInviteResource,
ProjectResource,
ServerResource,
SubscriptionResource,
VersionResource,
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
@@ -42,8 +35,7 @@ class SpeckleClient(CoreSpeckleClient):
client = SpeckleClient(host="app.speckle.systems") # 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)
# authenticate the client with an account (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate_with_account(account)
@@ -72,64 +64,32 @@ class SpeckleClient(CoreSpeckleClient):
self.account = Account()
def _init_resources(self) -> None:
self.server = ServerResource(
self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
with contextlib.suppress(Exception):
try:
server_version = self.server.version()
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
except Exception:
pass
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,
+2 -5
View File
@@ -2,11 +2,8 @@ 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 ( # noqa: F401
Account,
StreamWrapper, # noqa: F401
UserInfo,
)
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,
)
+1 -5
View File
@@ -53,9 +53,7 @@ def receive(
return _untracked_receive(obj_id, remote_transport, local_transport)
def serialize(
base: Base, write_transports: List[AbstractTransport] | None = None
) -> str:
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
"""
Serialize a base object. If no write transports are provided,
the object will be serialized
@@ -69,8 +67,6 @@ def serialize(
Returns:
str -- the serialized object
"""
if not write_transports:
write_transports = []
metrics.track(metrics.SDK, custom_props={"name": "Serialize"})
return core_serialize(base, write_transports)
+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)
@@ -1,24 +1,12 @@
from datetime import datetime
from typing import List, Optional, overload
from typing import List, Optional
from deprecated import deprecated
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, 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.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.active_user import Resource as CoreResource
from specklepy.logging import metrics
class ActiveUserResource(CoreResource):
class Resource(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."""
@@ -31,62 +19,37 @@ class ActiveUserResource(CoreResource):
)
self.schema = User
def get(self) -> Optional[User]:
metrics.track(metrics.SDK, self.account, {"name": "Active User Get"})
def get(self) -> User:
"""Gets the profile of the current authenticated user's profile
(as extracted from the authorization header).
Returns:
User -- the retrieved user
"""
metrics.track(metrics.SDK, custom_props={"name": "User Active 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: ...
):
"""Updates your user profile. All arguments are optional.
@overload
def update(self, *, input: UserUpdateInput) -> User: ...
Args:
name (Optional[str]): The user's name.
company (Optional[str]): The company the user works for.
bio (Optional[str]): A brief user biography.
avatar (Optional[str]): A URL to an avatar image for the 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,
)
)
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 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,
@@ -115,7 +78,6 @@ class ActiveUserResource(CoreResource):
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.
@@ -127,7 +89,6 @@ class ActiveUserResource(CoreResource):
)
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]:
@@ -138,8 +99,7 @@ class ActiveUserResource(CoreResource):
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.
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,13 +1,7 @@
from typing import Optional, Union
from deprecated import deprecated
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.core.api.resources.branch import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
@@ -23,7 +17,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,7 +32,6 @@ 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]:
@@ -56,7 +48,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 +62,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 +83,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,13 +1,7 @@
from typing import List, Optional, Union
from deprecated import deprecated
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.core.api.resources.commit import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
@@ -23,7 +17,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 +31,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 +45,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,
@@ -86,7 +77,6 @@ class Resource(CoreResource):
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 +93,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 +108,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,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,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 ServerResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""Renamed to ServerResource"""
@@ -1,12 +1,6 @@
from typing import Dict, List
from deprecated import deprecated
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.core.api.resources.object import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.objects.base import Base
@@ -22,7 +16,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 +30,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()`.
@@ -1,29 +1,17 @@
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.api.models import ActivityCollection, LimitedUser
from specklepy.core.api.resources.other_user import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class OtherUserResource(CoreResource):
class Resource(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.
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:
@@ -31,29 +19,23 @@ class OtherUserResource(CoreResource):
account=account,
basepath=basepath,
client=client,
server_version=(server_version,),
server_version=server_version,
)
self.schema = LimitedUser
def get(self, id: str) -> Optional[LimitedUser]:
def get(self, id: str) -> LimitedUser:
"""
Retrieves the profile of a user specified by their user ID.
Args:
id (str): The unique identifier of the user.
Returns:
LimitedUser: The profile of the user with limited information.
"""
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]:
@@ -66,8 +48,7 @@ class OtherUserResource(CoreResource):
limit (int): Maximum number of search results to return.
Returns:
Union[List[LimitedUser], SpeckleException]:
A list of users matching the search
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:
@@ -78,7 +59,6 @@ class OtherUserResource(CoreResource):
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,
@@ -89,8 +69,8 @@ class OtherUserResource(CoreResource):
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Retrieves a collection of activities for a specified user,
with optional filters for activity type, time frame, and pagination.
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.
@@ -101,8 +81,7 @@ class OtherUserResource(CoreResource):
cursor (Optional[datetime]): Timestamp for pagination cursor.
Returns:
ActivityCollection: A collection of user activities filtered
according to specified criteria.
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,11 +1,11 @@
from typing import Any, Dict, List, Tuple
from specklepy.api.models import ServerInfo
from specklepy.core.api.resources import ServerResource as CoreResource
from specklepy.core.api.resources.server import Resource as CoreResource
from specklepy.logging import metrics
class ServerResource(CoreResource):
class Resource(CoreResource):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
@@ -31,8 +31,7 @@ class ServerResource(CoreResource):
the server version in the format (major, minor, patch, (tag, build))
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
"""
# not tracking as it will be called along with other
# mutations / queries as a check
# not tracking as it will be called along with other mutations / queries as a check
return super().version()
def apps(self) -> Dict:
@@ -1,14 +1,8 @@
from datetime import datetime
from typing import List, Optional
from deprecated import deprecated
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.core.api.resources.stream import Resource as CoreResource
from specklepy.logging import metrics
@@ -25,7 +19,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 +33,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 +45,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 +65,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 +87,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 +99,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 +120,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 +134,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 +152,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 +179,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 +205,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 +220,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 +237,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
@@ -276,7 +257,6 @@ class Resource(CoreResource):
)
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 +270,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,
@@ -1,17 +1,24 @@
from functools import wraps
from typing import Callable, Dict, List, Optional, Union
from deprecated import deprecated
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.core.api.resources.subscriptions import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
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 +31,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 +46,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):
"""
@@ -61,7 +66,6 @@ class Resource(CoreResource):
)
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.
@@ -83,7 +87,6 @@ class Resource(CoreResource):
)
return super().stream_removed(callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def subscribe(
self,
@@ -4,7 +4,7 @@ from typing import List, Optional, Union
from deprecated import deprecated
from specklepy.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.deprecated.user import Resource as CoreResource
from specklepy.core.api.resources.user import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
+38 -73
View File
@@ -1,4 +1,3 @@
import contextlib
import re
from typing import Dict
from warnings import warn
@@ -12,17 +11,12 @@ 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,
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
@@ -50,8 +44,7 @@ class SpeckleClient:
client = SpeckleClient(host="app.speckle.systems") # 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)
# authenticate the client with an account (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate_with_account(account)
@@ -104,8 +97,7 @@ class SpeckleClient:
self._init_resources()
# ? Check compatibility with the server
# - i think we can skip this at this point? save a request
# ? Check compatibility with the server - i think we can skip this at this point? save a request
# try:
# server_info = self.server.get()
# if isinstance(server_info, Exception):
@@ -184,80 +176,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 "
f"Speckle Client for server {self.url}"
),
stacklevel=2,
)
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
with contextlib.suppress(Exception):
try:
server_version = self.server.version()
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
except Exception:
pass
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,
@@ -285,7 +250,7 @@ class SpeckleClient:
return attr.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
except AttributeError as ex:
except AttributeError:
raise SpeckleException(
f"Method {name} is not supported by the SpeckleClient class"
) from ex
)
+3 -4
View File
@@ -12,11 +12,10 @@ 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):
@@ -59,7 +58,7 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
res = account_storage.get_all_objects()
account_storage.close()
if res:
accounts.extend(Account.model_validate_json(r[1]) for r in res)
accounts.extend(Account.parse_raw(r[1]) for r in res)
except SpeckleException:
# cannot open SQLiteTransport, probably because of the lack
# of disk write permissions
@@ -80,7 +79,7 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
if json_acct_files:
try:
accounts.extend(
Account.model_validate_json(Path(json_path, json_file).read_text())
Account.parse_raw(Path(json_path, json_file).read_text())
# Account.parse_file(os.path.join(json_path, json_file))
for json_file in json_acct_files
)
-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"
-42
View File
@@ -1,42 +0,0 @@
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
ModelVersionsFilter,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectInviteCreateInput,
ProjectInviteUseInput,
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.inputs.version_inputs import (
CreateVersionInput,
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
__all__ = [
"CreateModelInput",
"DeleteModelInput",
"UpdateModelInput",
"ModelVersionsFilter",
"ProjectCreateInput",
"ProjectInviteCreateInput",
"ProjectInviteUseInput",
"ProjectModelsFilter",
"ProjectUpdateInput",
"ProjectUpdateRoleInput",
"UserProjectsFilter",
"UserUpdateInput",
"UpdateVersionInput",
"MoveVersionsInput",
"DeleteVersionsInput",
"CreateVersionInput",
"MarkReceivedVersionInput",
]
@@ -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,47 +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]
@@ -1,15 +0,0 @@
from typing import Optional, Sequence
from pydantic import BaseModel
class UserUpdateInput(BaseModel):
avatar: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
name: Optional[str] = None
class UserProjectsFilter(BaseModel):
search: str
onlyWithRoles: Optional[Sequence[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,17 +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
@@ -19,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
@@ -44,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
@@ -60,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
@@ -68,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
@@ -102,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
@@ -130,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
@@ -145,3 +183,23 @@ class ActivityCollection(BaseModel):
def __str__(self) -> str:
return self.__repr__()
class ServerMigration(BaseModel):
movedTo: Optional[str] = None
movedFrom: Optional[str] = None
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
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = 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",
]
-172
View File
@@ -1,172 +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: Optional[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]
+2 -7
View File
@@ -70,8 +70,7 @@ def receive(
serializer = BaseObjectSerializer(read_transport=local_transport)
# try local transport first. if the parent is there, we assume all the children
# are there and continue with deserialization using the local transport
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport
obj_string = local_transport.get_object(obj_id)
if obj_string:
return serializer.read_json(obj_string=obj_string)
@@ -91,9 +90,7 @@ def receive(
return serializer.read_json(obj_string=obj_string)
def serialize(
base: Base, write_transports: List[AbstractTransport] | None = None
) -> str:
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
"""
Serialize a base object. If no write transports are provided,
the object will be serialized
@@ -107,8 +104,6 @@ def serialize(
Returns:
str -- the serialized object
"""
if not write_transports:
write_transports = []
serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1]
+2 -37
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,10 +14,8 @@ 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:
class ResourceBase(object):
def __init__(
self,
account: Account,
@@ -46,35 +43,6 @@ class ResourceBase:
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,9 +69,6 @@ class ResourceBase:
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,30 +1,21 @@
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.user_inputs import UserProjectsFilter, 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.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__(
@@ -36,75 +27,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,
@@ -112,125 +66,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,
@@ -238,7 +107,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.
@@ -309,7 +178,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
@@ -330,18 +198,13 @@ class ActiveUserResource(ResourceBase):
inviteId
streamId
streamName
projectId
projectName
title
role
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
@@ -354,7 +217,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]:
@@ -379,21 +241,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,13 +1,8 @@
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
@@ -15,10 +10,7 @@ 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 +21,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:
@@ -63,7 +54,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 +101,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 +156,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 +197,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,13 +1,8 @@
from typing import List, Optional, Union
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
@@ -15,10 +10,7 @@ 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 +21,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 +59,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 +100,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,
@@ -160,7 +149,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 +176,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 +200,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,299 +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,257 +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,345 +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,240 +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,95 +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]:
@@ -154,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
}
}
}
@@ -172,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,
@@ -11,7 +11,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:
@@ -80,8 +80,7 @@ class ServerResource(ResourceBase):
the server version in the format (major, minor, patch, (tag, build))
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
"""
# not tracking as it will be called along with other mutations / queries
# as a check
# not tracking as it will be called along with other mutations / queries as a check
query = gql(
"""
query Server {
@@ -1,7 +1,6 @@
from datetime import datetime, timezone
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import (
@@ -9,10 +8,6 @@ from specklepy.core.api.models import (
PendingStreamCollaborator,
Stream,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
@@ -20,10 +15,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 +28,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 +89,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
@@ -111,7 +101,7 @@ class Resource(ResourceBase):
query = gql(
"""
query User($stream_limit: Int!) {
activeUser {
user {
id
bio
name
@@ -149,10 +139,9 @@ class Resource(ResourceBase):
params = {"stream_limit": stream_limit}
return self.make_request(
query=query, params=params, return_type=["activeUser", "streams", "items"]
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",
@@ -187,7 +176,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 +216,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 +239,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 +318,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 +351,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 +378,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 +406,6 @@ class Resource(ResourceBase):
schema=PendingStreamCollaborator,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite(
self,
stream_id: str,
@@ -487,7 +462,6 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_batch(
self,
stream_id: str,
@@ -547,7 +521,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 +552,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 +589,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 +635,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 +664,6 @@ class Resource(ResourceBase):
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
stream_id: str,
@@ -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.
-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
+5 -16
View File
@@ -159,12 +159,11 @@ class StreamWrapper:
try:
self.branch_name = project["project"]["model"]["name"]
except KeyError as ke:
raise SpeckleException("Project model name is not found", ke) from ke
raise SpeckleException("Project model name is not found", ke)
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 {key_stream} id found."
)
@property
@@ -214,11 +213,7 @@ class StreamWrapper:
self._client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if self._account.token is None and token is None:
warn(
f"No local account found for server {self.host}",
SpeckleWarning,
stacklevel=2,
)
warn(f"No local account found for server {self.host}", SpeckleWarning)
return self._client
if self._account.token:
@@ -271,20 +266,14 @@ class StreamWrapper:
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}"
f"{self.stream_id}{key_branches}{value_branch}"
)
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}"
f"{key_branches}{value_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:
@@ -1,7 +1,6 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
@@ -99,7 +98,7 @@ def user_application_data_path() -> Path:
except Exception as ex:
raise SpeckleException(
message="Failed to initialize user application data path.", exception=ex
) from ex
)
def user_speckle_folder_path() -> Path:
+2 -3
View File
@@ -86,8 +86,7 @@ def track(
METRICS_TRACKER.queue.put_nowait(event_params)
except Exception as ex:
# wrapping this whole thing in a try except as we never want a failure here
# to annoy users!
# wrapping this whole thing in a try except as we never want a failure here to annoy users!
LOG.debug(f"Error queueing metrics request: {str(ex)}")
@@ -107,7 +106,7 @@ class Singleton(type):
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
+15
View File
@@ -0,0 +1,15 @@
from typing import Optional
from specklepy.objects.base import Base
class CRS(Base, speckle_type="Objects.GIS.CRS"):
"""A Coordinate Reference System stored in wkt format"""
name: Optional[str] = None
authority_id: Optional[str] = None
wkt: Optional[str] = None
units_native: Optional[str] = None
offset_x: Optional[float] = None
offset_y: Optional[float] = None
rotation: Optional[float] = None
+13
View File
@@ -0,0 +1,13 @@
from typing import List, Optional
from specklepy.objects.base import Base
class GisFeature(
Base, speckle_type="Objects.GIS.GisFeature", detachable={"displayValue"}
):
"""GIS Feature"""
geometry: Optional[List[Base]] = None
attributes: Base
displayValue: Optional[List[Base]] = None
+22
View File
@@ -0,0 +1,22 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.GIS.geometry import (
GisLineElement,
GisPointElement,
GisPolygonElement,
GisPolygonGeometry,
GisRasterElement,
)
from specklepy.objects.GIS.layers import RasterLayer, VectorLayer
__all__ = [
"VectorLayer",
"RasterLayer",
"GisPolygonGeometry",
"GisPolygonElement",
"GisLineElement",
"GisPointElement",
"GisRasterElement",
"CRS",
]
+74
View File
@@ -0,0 +1,74 @@
from typing import List, Optional, Union
from specklepy.objects.base import Base
from specklepy.objects.geometry import (
Arc,
Circle,
Line,
Mesh,
Point,
Polycurve,
Polyline,
)
class GisPolygonGeometry(
Base, speckle_type="Objects.GIS.PolygonGeometry", detachable={"displayValue"}
):
"""GIS Polygon Geometry"""
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"""
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
class GisPointElement(Base, speckle_type="Objects.GIS.PointElement"):
"""GIS Point element"""
geometry: Optional[List[Point]] = None
attributes: Optional[Base] = None
class GisRasterElement(
Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}
):
"""GIS Raster element"""
band_count: Optional[int] = None
band_names: Optional[List[str]] = None
x_origin: Optional[float] = None
y_origin: Optional[float] = None
x_size: Optional[int] = None
y_size: Optional[int] = None
x_resolution: Optional[float] = None
y_resolution: Optional[float] = None
noDataValue: Optional[List[float]] = None
displayValue: Optional[List[Mesh]] = None
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
+142
View File
@@ -0,0 +1,142 @@
from typing import Any, Dict, List, Optional, Union
from deprecated import deprecated
from specklepy.objects.base import Base
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.other import Collection
@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,
units: str = "m",
features: Optional[List[Base]] = None,
layerType: str = "None",
geomType: str = "None",
renderer: Optional[Dict[str, Any]] = None,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.name = name
self.crs = crs
self.units = units
self.type = layerType
self.features = features or []
self.geomType = geomType
self.renderer = renderer or {}
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class VectorLayer(
Collection,
detachable={"elements"},
speckle_type="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
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class RasterLayer(
Collection,
detachable={"elements"},
speckle_type="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
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
+20 -3
View File
@@ -1,6 +1,23 @@
from .data_objects import DataObject, QgisObject
"""Builtin Speckle object kit."""
from specklepy.objects import (
GIS,
encoding,
geometry,
other,
primitive,
structural,
units,
)
from specklepy.objects.base import Base
__all__ = [
"DataObject",
"QgisObject",
"Base",
"encoding",
"geometry",
"other",
"units",
"structural",
"primitive",
"GIS",
]
+36 -35
View File
@@ -1,5 +1,4 @@
import contextlib
from dataclasses import dataclass, field
from enum import Enum
from inspect import isclass
from typing import (
@@ -19,7 +18,8 @@ from warnings import warn
from stringcase import pascalcase
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.units import Units
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
@@ -65,6 +65,7 @@ REMOVE_FROM_DIR = {
"_handle_object_count",
"_type_check",
"_type_registry",
"_units",
"add_chunkable_attrs",
"add_detachable_attrs",
"get_children_count",
@@ -115,7 +116,7 @@ class _RegisteringBase:
@classmethod
def _determine_speckle_type(cls) -> str:
"""
This method brings the speckle_type construction in par with Speckle-sharp/Core.
This method brings the speckle_type construction in par with peckle-sharp/Core.
The implementation differs, because in Core the basis of the speckle_type if
type.FullName, which includes the dotnet namespace name too.
@@ -167,11 +168,8 @@ class _RegisteringBase:
initialization. This is reused to register each subclassing type into a class
level dictionary.
"""
# if not speckle_type:
# raise Exception("no type")
cls._speckle_type_override = speckle_type
cls.speckle_type = cls._determine_speckle_type()
# cls.speckle_type = speckle_type
if cls._full_name() in cls._type_registry:
raise ValueError(
f"The speckle_type: {speckle_type} is already registered for type: "
@@ -224,7 +222,7 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if isinstance(t, ForwardRef):
return True, value
origin = t.__origin__
origin = getattr(t, "__origin__")
# below is what in nicer for >= py38
# origin = get_origin(t)
@@ -289,7 +287,7 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if len(args) != len(value):
return False, value
values = []
for t_item, v_item in zip(args, value, strict=True):
for t_item, v_item in zip(args, value):
item_valid, item_value = _validate_type(t_item, v_item)
if not item_valid:
return False, value
@@ -321,17 +319,22 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
return False, value
@dataclass(kw_only=True)
class Base(_RegisteringBase, speckle_type="Base"):
class Base(_RegisteringBase):
id: Union[str, None] = None
# totalChildrenCount: Union[int, None] = None
totalChildrenCount: Union[int, None] = None
applicationId: Union[str, None] = None
_units: Union[None, str] = None
def __init__(self, **kwargs) -> None:
super().__init__()
for k, v in kwargs.items():
self.__setattr__(k, v)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(id: {self.id}, "
f"speckle_type: {self.speckle_type}, "
# f"totalChildrenCount: {self.totalChildrenCount})"
f"totalChildrenCount: {self.totalChildrenCount})"
)
def __str__(self) -> str:
@@ -372,8 +375,7 @@ class Base(_RegisteringBase, speckle_type="Base"):
if name == "speckle_type":
# not sure if we should raise an exception here??
# raise SpeckleException(
# "Cannot override the `speckle_type`."
# "This is set manually by the class or on deserialisation"
# "Cannot override the `speckle_type`. This is set manually by the class or on deserialisation"
# )
return
# if value is not None:
@@ -401,10 +403,7 @@ class Base(_RegisteringBase, speckle_type="Base"):
try:
cls._attr_types = get_type_hints(cls)
except Exception as e:
warn(
f"Could not update forward refs for class {cls.__name__}: {e}",
stacklevel=2,
)
warn(f"Could not update forward refs for class {cls.__name__}: {e}")
@classmethod
def validate_prop_name(cls, name: str) -> None:
@@ -463,22 +462,21 @@ class Base(_RegisteringBase, speckle_type="Base"):
"""
self._detachable = self._detachable.union(names)
# @property
# def units(self) -> Union[str, None]:
# return self._units
@property
def units(self) -> Union[str, None]:
return self._units
# @units.setter
# def units(self, value: Union[str, Units, None]):
# """While this property accepts any string value,
# geometry expects units to be specific strings (see Units enum)"""
# if isinstance(value, str) or value is None:
# self._units = value
# elif isinstance(value, Units):
# self._units = value.value
# else:
# raise SpeckleInvalidUnitException(
# f"Unknown type {type(value)} received for units"
# )
@units.setter
def units(self, value: Union[str, Units, None]):
"""While this property accepts any string value, geometry expects units to be specific strings (see Units enum)"""
if isinstance(value, str) or value is None:
self._units = value
elif isinstance(value, Units):
self._units = value.value
else:
raise SpeckleInvalidUnitException(
f"Unknown type {type(value)} received for units"
)
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
@@ -570,6 +568,9 @@ class Base(_RegisteringBase, speckle_type="Base"):
Base.update_forward_refs()
@dataclass(kw_only=True)
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: List[Any] = field(default_factory=list)
data: Union[List[Any], None] = None
def __init__(self) -> None:
super().__init__()
self.data = []
-81
View File
@@ -1,81 +0,0 @@
from dataclasses import dataclass, field
from typing import Dict, List
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IDataObject, IGisObject, IHasUnits
from specklepy.logging.exceptions import SpeckleException
@dataclass(kw_only=True)
class DataObject(
Base,
IDataObject,
speckle_type="Objects.Data.DataObject",
detachable={"displayValue"},
):
name: str
properties: Dict[str, object]
displayValue: List[Base]
_name: str = field(repr=False, init=False)
_properties: Dict[str, object] = field(repr=False, init=False)
_displayValue: List[Base] = field(repr=False, init=False)
@property
def name(self) -> str:
return self._name
@property
def properties(self) -> Dict[str, object]:
return self._properties
@property
def displayValue(self) -> List[Base]:
return self._displayValue
@name.setter
def name(self, value: str):
if isinstance(value, str):
self._name = value
else:
raise SpeckleException(
f"'name' value should be string, received {type(value)}"
)
@properties.setter
def properties(self, value: dict):
if isinstance(value, dict):
self._properties = value
else:
raise SpeckleException(
f"'properties' value should be Dict, received {type(value)}"
)
@displayValue.setter
def displayValue(self, value: list):
if isinstance(value, list):
self._displayValue = value
else:
raise SpeckleException(
f"'displayValue' value should be List, received {type(value)}"
)
@dataclass(kw_only=True)
class QgisObject(
DataObject, IGisObject, IHasUnits, speckle_type="Objects.Data.QgisObject"
):
type: str
_type: str = field(repr=False, init=False)
@property
def type(self) -> str:
return self._type
@type.setter
def type(self, value: str):
if isinstance(value, str):
self._type = value
else:
raise SpeckleException(
f"'type' value should be string, received {type(value)}"
)
+131
View File
@@ -0,0 +1,131 @@
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Type
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
class CurveTypeEncoding(int, Enum):
Arc = 0
Circle = 1
Curve = 2
Ellipse = 3
Line = 4
Polyline = 5
Polycurve = 6
@property
def object_class(self) -> Type:
from . import geometry
if self == self.Arc:
return geometry.Arc
elif self == self.Circle:
return geometry.Circle
elif self == self.Curve:
return geometry.Curve
elif self == self.Ellipse:
return geometry.Ellipse
elif self == self.Line:
return geometry.Line
elif self == self.Polyline:
return geometry.Polyline
elif self == self.Polycurve:
return geometry.Polycurve
raise SpeckleException(
f"No corresponding object class for CurveTypeEncoding: {self}"
)
def curve_from_list(args: List[float]):
curve_type = CurveTypeEncoding(args[0])
return curve_type.object_class.from_list(args)
class ObjectArray:
def __init__(self, data: Optional[list] = None) -> None:
self.data = data or []
@classmethod
def from_objects(cls, objects: List[Base]) -> "ObjectArray":
data_list = cls()
if not objects:
return data_list
speckle_type = objects[0].speckle_type
for obj in objects:
if speckle_type != obj.speckle_type:
raise SpeckleException(
"All objects in chunk should have the same speckle_type. "
f"Found {speckle_type} and {obj.speckle_type}"
)
data_list.encode_object(obj=obj)
return data_list
@staticmethod
def decode_data(
data: List[Any], decoder: Callable[[List[Any]], Base], **kwargs: Dict[str, Any]
) -> List[Base]:
bases: List[Base] = []
if not data:
return bases
index = 0
while index < len(data):
item_length = int(data[index])
item_start = index + 1
item_end = item_start + item_length
item_data = data[item_start:item_end]
index = item_end
decoded_data = decoder(item_data, **kwargs)
bases.append(decoded_data)
return bases
def decode(self, decoder: Callable[[List[Any]], Any], **kwargs: Dict[str, Any]):
return self.decode_data(data=self.data, decoder=decoder, **kwargs)
def encode_object(self, obj: Base):
encoded = obj.to_list()
encoded.insert(0, len(encoded))
self.data.extend(encoded)
class CurveArray(ObjectArray):
@classmethod
def from_curve(cls, curve: Base) -> "CurveArray":
crv_array = cls()
crv_array.data = curve.to_list()
return crv_array
@classmethod
def from_curves(cls, curves: List[Base]) -> "CurveArray":
data = []
for curve in curves:
curve_list = curve.to_list()
curve_list.insert(0, len(curve_list))
data.extend(curve_list)
crv_array = cls()
crv_array.data = data
return crv_array
@staticmethod
def curve_from_list(args: List[float]) -> Base:
curve_type = CurveTypeEncoding(args[0])
return curve_type.object_class.from_list(args)
@property
def type(self) -> CurveTypeEncoding:
return CurveTypeEncoding(self.data[0])
def to_curve(self) -> Base:
return self.type.object_class.from_list(self.data)
@classmethod
def _curve_decoder(cls, data: List[float]) -> Base:
crv_array = cls(data)
return crv_array.to_curve()
def to_curves(self) -> List[Base]:
return self.decode(decoder=self._curve_decoder)
@@ -3,7 +3,7 @@ from typing import List, Optional
from specklepy.objects.geometry import Point
from specklepy.objects.base import Base
from .base import Base
CHUNKABLE_PROPS = {
"vertices": 100,
@@ -16,9 +16,7 @@ CHUNKABLE_PROPS = {
DETACHABLE = {"detach_this", "origin", "detached_list"}
class FakeGeo(
Base, speckle_type="FakeGeo", chunkable={"dots": 50}, detachable={"pointslist"}
):
class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
pointslist: Optional[List[Base]] = None
dots: Optional[List[int]] = None
@@ -30,9 +28,7 @@ class FakeDirection(Enum):
WEST = 4
class FakeMesh(
FakeGeo, speckle_type="FakeMesh", chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE
):
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
vertices: Optional[List[float]] = None
faces: Optional[List[int]] = None
colors: Optional[List[int]] = None
+946
View File
@@ -0,0 +1,946 @@
from enum import Enum
from typing import Any, List, Optional
from specklepy.objects.base import Base
from specklepy.objects.encoding import CurveArray, CurveTypeEncoding, ObjectArray
from specklepy.objects.primitive import Interval
from specklepy.objects.units import get_encoding_from_units, get_units_from_encoding
GEOMETRY = "Objects.Geometry."
class Point(Base, speckle_type=GEOMETRY + "Point"):
x: float = 0.0
y: float = 0.0
z: float = 0.0
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id:"
f" {self.id}, speckle_type: {self.speckle_type})"
)
@classmethod
def from_list(cls, args: List[float]) -> "Point":
"""
Create a new Point from a list of three floats
representing the x, y, and z coordinates
"""
return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[Any]:
return [self.x, self.y, self.z]
@classmethod
def from_coords(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0):
"""Create a new Point from x, y, and z values"""
pt = Point()
pt.x, pt.y, pt.z = x, y, z
return pt
class Pointcloud(
Base,
speckle_type=GEOMETRY + "Pointcloud",
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
):
points: Optional[List[float]] = None
colors: Optional[List[int]] = None
sizes: Optional[List[float]] = None
bbox: Optional["Box"] = None
class Vector(Base, speckle_type=GEOMETRY + "Vector"):
x: float = 0.0
y: float = 0.0
z: float = 0.0
applicationId: Optional[str] = None
def __repr__(self) -> str:
return (
f"{self.__class__.__name__} "
"(x: {self.x}, y: {self.y}, z: {self.z}, id: {self.id}, "
"speckle_type: {self.speckle_type})"
)
@classmethod
def from_list(cls, args: List[float]) -> "Vector":
"""
Create from a list of three floats representing the x, y, and z coordinates.
"""
return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[float]:
return [self.x, self.y, self.z]
@classmethod
def from_coords(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> "Vector":
"""Create a new Point from x, y, and z values"""
v = Vector()
v.x, v.y, v.z = x, y, z
return v
class ControlPoint(Point, speckle_type=GEOMETRY + "ControlPoint"):
weight: Optional[float] = None
class Plane(Base, speckle_type=GEOMETRY + "Plane"):
origin: Point = Point()
normal: Vector = Vector()
xdir: Vector = Vector()
ydir: Vector = Vector()
@classmethod
def from_list(cls, args: List[Any]) -> "Plane":
return cls(
origin=Point.from_list(args[:3]),
normal=Vector.from_list(args[3:6]),
xdir=Vector.from_list(args[6:9]),
ydir=Vector.from_list(args[9:12]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
*self.origin.to_list(),
*self.normal.to_list(),
*self.xdir.to_list(),
*self.ydir.to_list(),
get_encoding_from_units(self._units),
]
class Box(Base, speckle_type=GEOMETRY + "Box"):
basePlane: Plane = Plane()
xSize: Interval = Interval()
ySize: Interval = Interval()
zSize: Interval = Interval()
area: Optional[float] = None
volume: Optional[float] = None
class Line(Base, speckle_type=GEOMETRY + "Line"):
start: Point = Point()
end: Optional[Point] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Line":
return cls(
start=Point.from_list(args[1:4]),
end=Point.from_list(args[4:7]),
domain=Interval.from_list(args[7:10]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
domain = self.domain.to_list() if self.domain else [0, 1]
return [
CurveTypeEncoding.Line.value,
*self.start.to_list(),
*self.end.to_list(),
*domain,
get_encoding_from_units(self._units),
]
class Arc(Base, speckle_type=GEOMETRY + "Arc"):
radius: Optional[float] = None
startAngle: Optional[float] = None
endAngle: Optional[float] = None
angleRadians: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
startPoint: Optional[Point] = None
midPoint: Optional[Point] = None
endPoint: Optional[Point] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Arc":
return cls(
radius=args[1],
startAngle=args[2],
endAngle=args[3],
angleRadians=args[4],
domain=Interval.from_list(args[5:7]),
plane=Plane.from_list(args[7:20]),
startPoint=Point.from_list(args[20:23]),
midPoint=Point.from_list(args[23:26]),
endPoint=Point.from_list(args[26:29]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Arc.value,
self.radius,
self.startAngle,
self.endAngle,
self.angleRadians,
*self.domain.to_list(),
*self.plane.to_list(),
*self.startPoint.to_list(),
*self.midPoint.to_list(),
*self.endPoint.to_list(),
get_encoding_from_units(self._units),
]
class Circle(Base, speckle_type=GEOMETRY + "Circle"):
radius: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Circle":
return cls(
radius=args[1],
domain=Interval.from_list(args[2:4]),
plane=Plane.from_list(args[4:17]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Circle.value,
self.radius,
*self.domain.to_list(),
*self.plane.to_list(),
get_encoding_from_units(self._units),
]
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
firstRadius: Optional[float] = None
secondRadius: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
trimDomain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Ellipse":
return cls(
firstRadius=args[1],
secondRadius=args[2],
domain=Interval.from_list(args[3:5]),
plane=Plane.from_list(args[5:18]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Ellipse.value,
self.firstRadius,
self.secondRadius,
*self.domain.to_list(),
*self.plane.to_list(),
get_encoding_from_units(self._units),
]
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
value: Optional[List[float]] = None
closed: Optional[bool] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_points(cls, points: List[Point]):
"""Create a new Polyline from a list of Points"""
polyline = cls()
polyline.units = points[0].units
polyline.value = []
for point in points:
polyline.value.extend([point.x, point.y, point.z])
return polyline
@classmethod
def from_list(cls, args: List[Any]) -> "Polyline":
point_count = args[4]
return cls(
closed=bool(args[1]),
domain=Interval.from_list(args[2:4]),
value=args[5 : 5 + point_count],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Polyline.value,
int(self.closed),
*self.domain.to_list(),
len(self.value),
*self.value,
get_encoding_from_units(self._units),
]
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.value:
return
if len(self.value) % 3:
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.value)
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
class SpiralType(Enum):
Biquadratic = 0
BiquadraticParabola = 1
Bloss = 2
Clothoid = 3
Cosine = 4
Cubic = 5
CubicParabola = 6
Radioid = 7
Sinusoid = 8
Unknown = 9
class Spiral(Base, speckle_type=GEOMETRY + "Spiral", detachable={"displayValue"}):
startPoint: Optional[Point] = None
endPoint: Optional[Point]
plane: Optional[Plane]
turns: Optional[float]
pitchAxis: Optional[Vector] = Vector()
pitch: float = 0
spiralType: Optional[SpiralType] = None
displayValue: Optional[Polyline] = None
bbox: Optional[Box] = None
length: Optional[float] = None
domain: Optional[Interval] = None
class Curve(
Base,
speckle_type=GEOMETRY + "Curve",
chunkable={"points": 20000, "weights": 20000, "knots": 20000},
):
degree: Optional[int] = None
periodic: Optional[bool] = None
rational: Optional[bool] = None
points: Optional[List[float]] = None
weights: Optional[List[float]] = None
knots: Optional[List[float]] = None
domain: Optional[Interval] = None
displayValue: Optional[Polyline] = None
closed: Optional[bool] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.points:
return
if len(self.points) % 3:
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.points)
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
@classmethod
def from_list(cls, args: List[Any]) -> "Curve":
point_count = int(args[7])
weights_count = int(args[8])
knots_count = int(args[9])
points_start = 10
weights_start = 10 + point_count
knots_start = weights_start + weights_count
knots_end = knots_start + knots_count
return cls(
degree=int(args[1]),
periodic=bool(args[2]),
rational=bool(args[3]),
closed=bool(args[4]),
domain=Interval.from_list(args[5:7]),
points=args[points_start:weights_start],
weights=args[weights_start:knots_start],
knots=args[knots_start:knots_end],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Curve.value,
self.degree,
int(self.periodic),
int(self.rational),
int(self.closed),
*self.domain.to_list(),
len(self.points),
len(self.weights),
len(self.knots),
*self.points,
*self.weights,
*self.knots,
get_encoding_from_units(self._units),
]
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: Optional[List[Base]] = None
domain: Optional[Interval] = None
closed: Optional[bool] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Polycurve":
curve_arrays = CurveArray(args[5:-1])
return cls(
closed=bool(args[1]),
domain=Interval.from_list(args[2:4]),
segments=curve_arrays.to_curves(),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
curve_array = CurveArray.from_curves(self.segments).data
return [
CurveTypeEncoding.Polycurve.value,
int(self.closed),
*self.domain.to_list(),
len(curve_array),
*curve_array,
get_encoding_from_units(self._units),
]
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
capped: Optional[bool] = None
profile: Optional[Base] = None
pathStart: Optional[Point] = None
pathEnd: Optional[Point] = None
pathCurve: Optional[Base] = None
pathTangent: Optional[Base] = None
profiles: Optional[List[Base]] = None
length: Optional[float] = None
area: Optional[float] = None
volume: Optional[float] = None
bbox: Optional[Box] = None
class Mesh(
Base,
speckle_type=GEOMETRY + "Mesh",
chunkable={
"vertices": 2000,
"faces": 2000,
"colors": 2000,
"textureCoordinates": 2000,
},
):
vertices: Optional[List[float]] = None
faces: Optional[List[int]] = None
colors: Optional[List[int]] = None
textureCoordinates: Optional[List[float]] = None
bbox: Optional[Box] = None
area: Optional[float] = None
volume: Optional[float] = None
@classmethod
def create(
cls,
vertices: List[float],
faces: List[int],
colors: Optional[List[int]] = None,
texture_coordinates: Optional[List[float]] = None,
) -> "Mesh":
"""
Create a new Mesh from lists representing its vertices, faces,
colors (optional), and texture coordinates (optional).
This will initialise empty lists for colors and texture coordinates
if you do not provide any.
"""
return cls(
vertices=vertices,
faces=faces,
colors=colors or [],
textureCoordinates=texture_coordinates or [],
)
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: Optional[int] = None
degreeV: Optional[int] = None
rational: Optional[bool] = None
area: Optional[float] = None
pointData: Optional[List[float]] = None
countU: Optional[int] = None
countV: Optional[int] = None
bbox: Optional[Box] = None
closedU: Optional[bool] = None
closedV: Optional[bool] = None
domainU: Optional[Interval] = None
domainV: Optional[Interval] = None
knotsU: Optional[List[float]] = None
knotsV: Optional[List[float]] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Surface":
point_count = int(args[11])
knots_u_count = int(args[12])
knots_v_count = int(args[13])
start_point_data = 14
start_knots_u = start_point_data + point_count
start_knots_v = start_knots_u + knots_u_count
return cls(
degreeU=int(args[0]),
degreeV=int(args[1]),
countU=int(args[2]),
countV=int(args[3]),
rational=bool(args[4]),
closedU=bool(args[5]),
closedV=bool(args[6]),
domainU=Interval(start=args[7], end=args[8]),
domainV=Interval(start=args[9], end=args[10]),
pointData=args[start_point_data:start_knots_u],
knotsU=args[start_knots_u:start_knots_v],
knotsV=args[start_knots_v : start_knots_v + knots_v_count],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
self.degreeU,
self.degreeV,
self.countU,
self.countV,
int(self.rational),
int(self.closedU),
int(self.closedV),
*self.domainU.to_list(),
*self.domainV.to_list(),
len(self.pointData),
len(self.knotsU),
len(self.knotsV),
*self.pointData,
*self.knotsU,
*self.knotsV,
get_encoding_from_units(self._units),
]
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
_Brep: Optional["Brep"] = None
SurfaceIndex: Optional[int] = None
OuterLoopIndex: Optional[int] = None
OrientationReversed: Optional[bool] = None
LoopIndices: Optional[List[int]] = None
@property
def _outer_loop(self):
return self._Brep.Loops[self.OuterLoopIndex] # pylint: disable=no-member
@property
def _surface(self):
return self._Brep.Surfaces[self.SurfaceIndex] # pylint: disable=no-member
@property
def _loops(self):
if self.LoopIndices:
# pylint: disable=not-an-iterable, no-member
return [self._Brep.Loops[i] for i in self.LoopIndices]
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepFace":
return cls(
_Brep=brep,
SurfaceIndex=args[0],
OuterLoopIndex=args[1],
OrientationReversed=bool(args[2]),
LoopIndices=args[3:],
)
def to_list(self) -> List[Any]:
return [
self.SurfaceIndex,
self.OuterLoopIndex,
int(self.OrientationReversed),
*self.LoopIndices,
]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
_Brep: Optional["Brep"] = None
Curve3dIndex: Optional[int] = None
TrimIndices: Optional[List[int]] = None
StartIndex: Optional[int] = None
EndIndex: Optional[int] = None
ProxyCurveIsReversed: Optional[bool] = None
Domain: Optional[Interval] = None
@property
def _start_vertex(self):
return self._Brep.Vertices[self.StartIndex]
@property
def _end_vertex(self):
return self._Brep.Vertices[self.EndIndex]
@property
def _trims(self):
if self.TrimIndices:
# pylint: disable=not-an-iterable
return [self._Brep.Trims[i] for i in self.TrimIndices]
@property
def _curve(self):
return self._Brep.Curve3D[self.Curve3dIndex]
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepEdge":
domain_start = args[4]
domain_end = args[5]
domain = (
Interval(start=domain_start, end=domain_end)
if None not in (domain_start, domain_end)
else None
)
return cls(
_Brep=brep,
Curve3dIndex=int(args[0]),
TrimIndices=[int(t) for t in args[6:]],
StartIndex=int(args[1]),
EndIndex=int(args[2]),
ProxyCurveIsReversed=bool(args[3]),
Domain=domain,
)
def to_list(self) -> List[Any]:
return [
self.Curve3dIndex,
self.StartIndex,
self.EndIndex,
int(self.ProxyCurveIsReversed),
self.Domain.start,
self.Domain.end,
*self.TrimIndices,
]
class BrepLoopType(int, Enum):
Unknown = 0
Outer = 1
Inner = 2
Slit = 3
CurveOnSurface = 4
PointOnSurface = 5
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
_Brep: Optional["Brep"] = None
FaceIndex: Optional[Optional[int]] = None
TrimIndices: Optional[List[int]] = None
Type: Optional[BrepLoopType] = None
@property
def _face(self):
return self._Brep.Faces[self.FaceIndex]
@property
def _trims(self):
if self.TrimIndices:
# pylint: disable=not-an-iterable
return [self._Brep.Trims[i] for i in self.TrimIndices]
@classmethod
def from_list(cls, args: List[any], brep: "Brep" = None):
return cls(
_Brep=brep,
FaceIndex=args[0],
Type=BrepLoopType(args[1]),
TrimIndices=args[2:],
)
def to_list(self) -> List[int]:
return [
self.FaceIndex,
self.Type.value,
*self.TrimIndices,
]
class BrepTrimType(int, Enum):
Unknown = 0
Boundary = 1
Mated = 2
Seam = 3
Singular = 4
CurveOnSurface = 5
PointOnSurface = 6
Slit = 7
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
_Brep: Optional["Brep"] = None
EdgeIndex: Optional[int] = None
StartIndex: Optional[int] = None
EndIndex: Optional[int] = None
FaceIndex: Optional[int] = None
LoopIndex: Optional[int] = None
CurveIndex: Optional[int] = None
IsoStatus: Optional[int] = None
TrimType: Optional[BrepTrimType] = None
IsReversed: Optional[bool] = None
Domain: Optional[Interval] = None
@property
def _face(self):
if self._Brep:
return self._Brep.Faces[self.FaceIndex] # pylint: disable=no-member
@property
def _loop(self):
if self._Brep:
return self._Brep.Loops[self.LoopIndex] # pylint: disable=no-member
@property
def _edge(self):
if self._Brep:
# pylint: disable=no-member
return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
@property
def _curve_2d(self):
if self._Brep:
return self._Brep.Curve2D[self.CurveIndex] # pylint: disable=no-member
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepTrim":
return cls(
_Brep=brep,
EdgeIndex=args[0],
StartIndex=args[1],
EndIndex=args[2],
FaceIndex=args[3],
LoopIndex=args[4],
CurveIndex=args[5],
IsoStatus=args[6],
TrimType=BrepTrimType(args[7]),
IsReversed=bool(args[8]),
)
def to_list(self) -> List[Any]:
return [
self.EdgeIndex,
self.StartIndex,
self.EndIndex,
self.FaceIndex,
self.LoopIndex,
self.CurveIndex,
self.IsoStatus,
self.TrimType.value,
int(self.IsReversed),
]
class Brep(
Base,
speckle_type=GEOMETRY + "Brep",
chunkable={
"SurfacesValue": 31250,
"Curve3DValues": 31250,
"Curve2DValues": 31250,
"VerticesValue": 31250,
"EdgesValue": 62500,
"LoopsValue": 62500,
"FacesValue": 62500,
"TrimsValue": 62500,
},
detachable={"displayValue"},
serialize_ignore={
"Surfaces",
"Curve3D",
"Curve2D",
"Vertices",
"Trims",
"Edges",
"Loops",
"Faces",
},
):
provenance: Optional[str] = None
bbox: Optional[Box] = None
area: Optional[float] = None
volume: Optional[float] = None
_displayValue: Optional[List[Mesh]] = None
Surfaces: Optional[List[Surface]] = None
Curve3D: Optional[List[Base]] = None
Curve2D: Optional[List[Base]] = None
Vertices: Optional[List[Point]] = None
Edges: Optional[List[BrepEdge]] = None
Loops: Optional[List[BrepLoop]] = None
Faces: Optional[List[BrepFace]] = None
Trims: Optional[List[BrepTrim]] = None
IsClosed: Optional[bool] = None
Orientation: Optional[int] = None
def _inject_self_into_children(self, children: Optional[List[Base]]) -> List[Base]:
if children is None:
return children
for child in children:
child._Brep = self # pylint: disable=protected-access
return children
# set as prop for now for backwards compatibility
@property
def displayValue(self) -> List[Mesh]:
return self._displayValue
@displayValue.setter
def displayValue(self, value):
if isinstance(value, Mesh):
self._displayValue = [value]
elif isinstance(value, list):
self._displayValue = value
@property
def EdgesValue(self) -> List[BrepEdge]:
return None if self.Edges is None else ObjectArray.from_objects(self.Edges).data
@EdgesValue.setter
def EdgesValue(self, value: List[float]):
if not value:
return
self.Edges = ObjectArray.decode_data(value, BrepEdge.from_list, brep=self)
@property
def LoopsValue(self) -> List[BrepLoop]:
return None if self.Loops is None else ObjectArray.from_objects(self.Loops).data
@LoopsValue.setter
def LoopsValue(self, value: List[int]):
if not value:
return
self.Loops = ObjectArray.decode_data(value, BrepLoop.from_list, brep=self)
@property
def FacesValue(self) -> List[int]:
return None if self.Faces is None else ObjectArray.from_objects(self.Faces).data
@FacesValue.setter
def FacesValue(self, value: List[int]):
if not value:
return
self.Faces = ObjectArray.decode_data(value, BrepFace.from_list, brep=self)
@property
def SurfacesValue(self) -> List[float]:
return (
None
if self.Surfaces is None
else ObjectArray.from_objects(self.Surfaces).data
)
@SurfacesValue.setter
def SurfacesValue(self, value: List[float]):
if not value:
return
self.Surfaces = ObjectArray.decode_data(value, Surface.from_list)
@property
def Curve3DValues(self) -> List[float]:
return (
None if self.Curve3D is None else CurveArray.from_curves(self.Curve3D).data
)
@Curve3DValues.setter
def Curve3DValues(self, value: List[float]):
crv_array = CurveArray(value)
self.Curve3D = crv_array.to_curves()
@property
def Curve2DValues(self) -> List[Base]:
return (
None if self.Curve2D is None else CurveArray.from_curves(self.Curve2D).data
)
@Curve2DValues.setter
def Curve2DValues(self, value: List[float]):
crv_array = CurveArray(value)
self.Curve2D = crv_array.to_curves()
@property
def VerticesValue(self) -> List[Point]:
if self.Vertices is None:
return None
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
values = [encoded_unit]
for vertex in self.Vertices:
values.extend(vertex.to_list())
return values
@VerticesValue.setter
def VerticesValue(self, value: List[float]):
value = value.copy()
units = get_units_from_encoding(value.pop(0))
vertices = []
for i in range(0, len(value), 3):
vertex = Point.from_list(value[i : i + 3])
vertex.units = units
vertices.append(vertex)
self.Vertices = vertices
# TODO: can this be consistent with loops, edges, faces, curves, etc and prepend with the chunk list? needs to happen in sharp first
@property
def TrimsValue(self) -> List[float]:
# return None if self.Trims is None else ObjectArray.from_objects(self.Trims).data
if not self.Trims:
return
value = []
for trim in self.Trims:
value.extend(trim.to_list())
return value
@TrimsValue.setter
def TrimsValue(self, value: List[float]):
if not value:
return
# self.Trims = ObjectArray.decode_data(value, BrepTrim.from_list, brep=self)
self.Trims = [
BrepTrim.from_list(value[i : i + 9], self) for i in range(0, len(value), 9)
]
BrepEdge.update_forward_refs()
BrepLoop.update_forward_refs()
BrepTrim.update_forward_refs()
BrepFace.update_forward_refs()
@@ -1,18 +0,0 @@
from specklepy.objects.geometry.arc import Arc
from specklepy.objects.geometry.line import Line
from specklepy.objects.geometry.mesh import Mesh
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.geometry.vector import Vector
# re-export them at the geometry package level
__all__ = [
"Arc",
"Line",
"Mesh",
"Plane",
"Point",
"Polyline",
"Vector"
]
-36
View File
@@ -1,36 +0,0 @@
import math
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import ICurve, IHasUnits
@dataclass(kw_only=True)
class Arc(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Arc"):
plane: Plane
startPoint: Point
midPoint: Point
endPoint: Point
@property
def radius(self) -> float:
return self.startPoint.distance_to(self.plane.origin)
@property
def length(self) -> float:
start_to_mid = self.startPoint.distance_to(self.midPoint)
mid_to_end = self.midPoint.distance_to(self.endPoint)
r = self.radius
angle = (2 * math.asin(start_to_mid / (2 * r))) + \
(2 * math.asin(mid_to_end / (2 * r)))
return r * angle
@property
def measure(self) -> float:
start_to_mid = self.startPoint.distance_to(self.midPoint)
mid_to_end = self.midPoint.distance_to(self.endPoint)
r = self.radius
return (2 * math.asin(start_to_mid / (2 * r))) + \
(2 * math.asin(mid_to_end / (2 * r)))
-20
View File
@@ -1,20 +0,0 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import ICurve, IHasUnits
@dataclass(kw_only=True)
class Line(
Base,
IHasUnits,
ICurve,
speckle_type="Objects.Geometry.Line"
):
start: Point
end: Point
@property
def length(self) -> float:
return self.start.distance_to(self.end)
-211
View File
@@ -1,211 +0,0 @@
from dataclasses import dataclass, field
from typing import List, Tuple
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import IHasArea, IHasUnits, IHasVolume
@dataclass(kw_only=True)
class Mesh(
Base,
IHasArea,
IHasVolume,
IHasUnits,
speckle_type="Objects.Geometry.Mesh",
detachable={"vertices", "faces", "colors", "textureCoordinates"},
chunkable={
"vertices": 31250,
"faces": 62500,
"colors": 62500,
"textureCoordinates": 31250,
},
serialize_ignore={"vertices_count", "texture_coordinates_count"},
):
"""
a 3D mesh consisting of vertices and faces with optional colors and texture coordinates
"""
vertices: List[float]
faces: List[int]
colors: List[int] = field(default_factory=list)
textureCoordinates: List[float] = field(default_factory=list)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"vertices: {self.vertices_count}, "
f"faces: {self.faces_count}, "
f"units: {self.units}, "
f"has_colors: {len(self.colors) > 0}, "
f"has_texture_coords: {len(self.textureCoordinates) > 0})"
)
@property
def vertices_count(self) -> int:
"""
get the number of vertices in the mesh
"""
if len(self.vertices) % 3 != 0:
raise ValueError(
f"Invalid vertices list: length ({len(
self.vertices)}) must be a multiple of 3"
)
return len(self.vertices) // 3
@property
def texture_coordinates_count(self) -> int:
"""
get the number of texture coordinates in the mesh
"""
return len(self.textureCoordinates) // 2
@property
def area(self) -> float:
return self.__dict__.get('_area', 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__['_area'] = value
@property
def volume(self) -> float:
return self.__dict__.get('_volume', 0.0)
@volume.setter
def volume(self, value: float) -> None:
self.__dict__['_volume'] = value
def calculate_area(self) -> float:
"""
calculate total surface area of the mesh
"""
total_area = 0.0
face_index = 0
i = 0
while i < len(self.faces):
vertex_count = self.faces[i]
if vertex_count >= 3:
face_vertices = self.get_face_vertices(face_index)
for j in range(1, vertex_count - 1):
v0 = face_vertices[0]
v1 = face_vertices[j]
v2 = face_vertices[j + 1]
a = [v1.x - v0.x, v1.y - v0.y, v1.z - v0.z]
b = [v2.x - v0.x, v2.y - v0.y, v2.z - v0.z]
cx = a[1] * b[2] - a[2] * b[1]
cy = a[2] * b[0] - a[0] * b[2]
cz = a[0] * b[1] - a[1] * b[0]
area = 0.5 * (cx * cx + cy * cy + cz * cz) ** 0.5
total_area += area
i += vertex_count + 1
face_index += 1
return total_area
def calculate_volume(self) -> float:
"""
calculate volume of the mesh if it is closed
"""
if not self.is_closed():
return 0.0
total_volume = 0.0
face_index = 0
i = 0
while i < len(self.faces):
vertex_count = self.faces[i]
if vertex_count >= 3:
face_vertices = self.get_face_vertices(face_index)
v0 = face_vertices[0]
for j in range(1, vertex_count - 1):
v1 = face_vertices[j]
v2 = face_vertices[j + 1]
a = [v0.x, v0.y, v0.z]
b = [v1.x - v0.x, v1.y - v0.y, v1.z - v0.z]
c = [v2.x - v0.x, v2.y - v0.y, v2.z - v0.z]
cx = b[1] * c[2] - b[2] * c[1]
cy = b[2] * c[0] - b[0] * c[2]
cz = b[0] * c[1] - b[1] * c[0]
v = (a[0] * cx + a[1] * cy + a[2] * cz) / 6.0
total_volume += v
i += vertex_count + 1
face_index += 1
return abs(total_volume)
def get_point(self, index: int) -> Point:
"""
get vertex at index as a Point object
"""
if index < 0 or index >= self.vertices_count:
raise IndexError(f"Vertex index {index} out of range")
index *= 3
return Point(
x=self.vertices[index],
y=self.vertices[index + 1],
z=self.vertices[index + 2],
units=self.units,
)
def get_points(self) -> List[Point]:
"""
get all vertices as Point objects
"""
return [self.get_point(i) for i in range(self.vertices_count)]
def get_texture_coordinate(self, index: int) -> Tuple[float, float]:
"""
get texture coordinate at index
"""
if index < 0 or index >= self.texture_coordinates_count:
raise IndexError(f"Texture coordinate index {index} out of range")
index *= 2
return (self.textureCoordinates[index], self.textureCoordinates[index + 1])
def get_face_vertices(self, face_index: int) -> List[Point]:
"""
get the vertices of a specific face
"""
i = 0
current_face = 0
while i < len(self.faces):
if current_face == face_index:
vertex_count = self.faces[i]
vertices = []
for j in range(vertex_count):
vertex_index = self.faces[i + j + 1]
if vertex_index >= self.vertices_count:
raise IndexError(
f"Vertex index {vertex_index} out of range")
vertices.append(self.get_point(vertex_index))
return vertices
vertex_count = self.faces[i]
i += vertex_count + 1
current_face += 1
raise IndexError(f"Face index {face_index} out of range")
def is_closed(self) -> bool:
"""
check if the mesh is closed (verifying each edge appears twice)
"""
edge_counts = {}
i = 0
while i < len(self.faces):
vertex_count = self.faces[i]
for j in range(vertex_count):
v1 = self.faces[i + 1 + j]
v2 = self.faces[i + 1 + ((j + 1) % vertex_count)]
edge = tuple(sorted([v1, v2]))
edge_counts[edge] = edge_counts.get(edge, 0) + 1
i += vertex_count + 1
return all(count == 2 for count in edge_counts.values())
-28
View File
@@ -1,28 +0,0 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.vector import Vector
from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class Plane(Base, IHasUnits, speckle_type="Objects.Geometry.Plane"):
"""
a plane consisting of an origin Point, and 3 Vectors as its X, Y and Z axis.
"""
origin: Point
normal: Vector
xdir: Vector
ydir: Vector
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"origin: {self.origin}, "
f"normal: {self.normal}, "
f"xdir: {self.xdir}, "
f"ydir: {self.ydir}, "
f"units: {self.units})"
)
-33
View File
@@ -1,33 +0,0 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class Point(Base, IHasUnits, speckle_type="Objects.Geometry.Point"):
"""
a 3-dimensional point
"""
x: float
y: float
z: float
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, units: {self.units})"
def distance_to(self, other: "Point") -> float:
"""
calculates the distance between this point and another given point.
"""
if not isinstance(other, Point):
raise TypeError(f"Expected Point object, got {type(other)}")
# we assume that host application units are the same for both points
# unit conversion could be expensive, so we avoid it here
dx = other.x - self.x
dy = other.y - self.y
dz = other.z - self.z
return (dx * dx + dy * dy + dz * dz) ** 0.5
@@ -1,76 +0,0 @@
from dataclasses import dataclass
from typing import List
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import ICurve, IHasUnits
@dataclass(kw_only=True)
class Polyline(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Polyline"):
"""
a polyline curve, defined by a set of vertices.
"""
value: List[float]
def __repr__(self) -> str:
return f"{self.__class__.__name__}(value: {self.value}, units: {self.units})"
def is_closed(self, tolerance: float = 1e-6) -> bool:
"""
check if the polyline is closed (start point equals end point within tolerance)
"""
if len(self.value) < 6: # need at least 2 points to be closed
return False
# compare first and last points
start = Point(
x=self.value[0],
y=self.value[1],
z=self.value[2],
units=self.units
)
end = Point(
x=self.value[-3],
y=self.value[-2],
z=self.value[-1],
units=self.units
)
return start.distance_to(end) <= tolerance
@property
def length(self) -> float:
return self.__dict__.get('_length', 0.0)
@length.setter
def length(self, value: float) -> None:
self.__dict__['_length'] = value
def calculate_length(self) -> float:
points = self.get_points()
total_length = 0.0
for i in range(len(points) - 1):
total_length += points[i].distance_to(points[i + 1])
if self.is_closed() and points:
total_length += points[-1].distance_to(points[0])
return total_length
def get_points(self) -> List[Point]:
"""
converts the raw coordinate list into Point objects
"""
if len(self.value) % 3 != 0:
raise ValueError(
"Polyline value list is malformed: expected length to be multiple of 3"
)
points = []
for i in range(0, len(self.value), 3):
point = Point(
x=self.value[i],
y=self.value[i + 1],
z=self.value[i + 2],
units=self.units
)
points.append(point)
return points
-22
View File
@@ -1,22 +0,0 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class Vector(Base, IHasUnits, speckle_type="Objects.Geometry.Vector", serialize_ignore = {"length"}):
"""
a 3-dimensional vector
"""
x: float
y: float
z: float
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, units: {self.units})"
@property
def length(self) -> float:
return (self.x ** 2 + self.y ** 2 + self.z ** 2) ** 0.5
@@ -58,7 +58,9 @@ class CommitObjectBuilder(ABC, Generic[T]):
if parent_id == ROOT:
parent = root_commit_object
else:
parent = self.converted.get(parent_id, None)
parent = (
self.converted[parent_id] if parent_id in self.converted else None
)
if not parent:
continue
@@ -72,15 +74,13 @@ class CommitObjectBuilder(ABC, Generic[T]):
elements.append(current)
return
except Exception as ex:
# A parent was found, but it was invalid
# (Likely because of a type mismatch on a `elements` property)
# 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)}."
f"Checked {len(parents)} potential parent, and non were converted!"
f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!"
)
@@ -1,29 +0,0 @@
from specklepy.objects.base import Base
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
def has_display_value(x: Base):
return any(hasattr(x, alias) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def create_default_traversal_function() -> GraphTraversal:
"""
Traversal func for traversing the root object of a Speckle Model
"""
convertible_rule = TraversalRule(
[lambda b: b.speckle_type != "Base", has_display_value],
lambda _: ELEMENTS_PROPERTY_ALIASES,
)
default_rule = TraversalRule(
[lambda _: True],
# NOTE: Unlike the C# implementation, this does not ignore Obsolete members
lambda o: o.get_member_names(),
False,
)
return GraphTraversal([convertible_rule, default_rule])
@@ -7,10 +7,6 @@ from specklepy.objects.base import Base
class ITraversalRule(Protocol):
@property
def should_return(self) -> bool:
pass
def get_members_to_traverse(self, o: Base) -> Set[str]:
"""Get the members to traverse."""
pass
@@ -54,27 +50,20 @@ class GraphTraversal:
while len(stack) > 0:
head = stack.pop()
yield head
current = head.current
active_rule = self._get_active_rule_or_default_rule(current)
if active_rule.should_return:
yield head
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"}:
# debug: to avoid noisy exceptions,
# explicitly avoid checking ones we know will fail,
# this is not exhaustive
continue
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:
# Unset application ids, and class variables like SpeckleType will
# throw when __getitem__ is called
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
pass
@staticmethod
@@ -125,14 +114,12 @@ class GraphTraversal:
class TraversalRule:
_conditions: Collection[Callable[[Base], bool]]
_members_to_traverse: Callable[[Base], Iterable[str]]
_should_return_to_output: bool = True
@property
def should_return(self) -> bool:
return self._should_return_to_output
def get_members_to_traverse(self, o: Base) -> Set[str]:
return set(self._members_to_traverse(o))
def does_rule_hold(self, o: Base) -> bool:
return any(condition(o) for condition in self._conditions)
for condition in self._conditions:
if condition(o):
return True
return False
-122
View File
@@ -1,122 +0,0 @@
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass, field
from typing import Dict, Generic, List, TypeVar
from specklepy.logging.exceptions import SpeckleInvalidUnitException
from specklepy.objects.base import Base
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
T = TypeVar("T") # define type variable for generic type
# generic interfaces
@dataclass(kw_only=True)
class ICurve(metaclass=ABCMeta):
_domain: Interval = field(default_factory=Interval.unit_interval, init=False)
@property
@abstractmethod
def length(self) -> float:
pass
@property
def domain(self) -> Interval:
return self._domain
@domain.setter
def domain(self, value: Interval) -> None:
if not isinstance(value, Interval):
raise TypeError(f"Domain must be an Interval, got {type(value)}")
self._domain = value
class IDisplayValue(Generic[T], metaclass=ABCMeta):
@property
@abstractmethod
def displayValue(self) -> T:
pass
# field interfaces
@dataclass(kw_only=True)
class IHasUnits(metaclass=ABCMeta):
units: str | Units
_units: str = field(repr=False, init=False)
@property
def units(self) -> str:
return self._units
@units.setter
def units(self, value: str | Units):
if isinstance(value, str):
self._units = value
elif isinstance(value, Units):
self._units = value.value
else:
raise SpeckleInvalidUnitException(
f"Unknown type {type(value)} received for units"
)
@dataclass(kw_only=True)
class IHasArea(metaclass=ABCMeta):
_area: float = field(init=False, repr=False)
@property
def area(self) -> float:
return self._area
@area.setter
def area(self, value: float):
if not isinstance(value, (int, float)):
raise ValueError(f"Area must be a number, got {type(value)}")
self._area = float(value)
@dataclass(kw_only=True)
class IHasVolume(metaclass=ABCMeta):
_volume: float = field(init=False, repr=False)
@property
def volume(self) -> float:
return self._volume
@volume.setter
def volume(self, value: float):
if not isinstance(value, (int, float)):
raise ValueError(f"Volume must be a number, got {type(value)}")
self._volume = float(value)
# data object interfaces
class IProperties(metaclass=ABCMeta):
@property
@abstractmethod
def properties(self) -> Dict[str, object]:
pass
class IDataObject(IProperties, IDisplayValue[List[Base]], metaclass=ABCMeta):
@property
@abstractmethod
def name(self) -> str:
pass
class IBlenderObject(IDataObject, metaclass=ABCMeta):
@property
@abstractmethod
def type(self) -> str:
pass
class IGisObject(IDataObject, metaclass=ABCMeta):
@property
@abstractmethod
def type(self) -> str:
pass
@@ -1,37 +0,0 @@
from dataclasses import dataclass, field
from typing import List
from specklepy.objects.base import Base
@dataclass(kw_only=True)
class Collection(
Base,
# TODO: add deprecated speckle_types
speckle_type="Speckle.Core.Models.Collections.Collection",
detachable={"elements"},
):
"""
A simple container for organising objects within a model
and preserving object hierarchy.
A container is defined by a human-readable name a unique applicationId and
its list of contained elements.
The elements can include an unrestricted number of Base objects including
additional nested Collections.
Note:
A Collection can be for example a Layer in Rhino/AutoCad,
a collection in Blender, or a Category in Revit.
The location of each collection in the hierarchy of collections in a commit
will be retrieved through commit traversal.
Attributes:
name: The human-readable name of the Collection. This name is not necessarily
unique within a commit. Set the applicationId for a unique identifier.
elements: The elements contained in this Collection.
This may include additional nested Collections
"""
name: str
elements: List[Base] = field(default_factory=list)
@@ -1,18 +0,0 @@
from abc import ABCMeta, abstractmethod
class ISpeckleObject(speckle_type="ISpeckleObjects", metaclass=ABCMeta):
@property
@abstractmethod
def id(self) -> str | None:
pass
@property
@abstractmethod
def application_id(self) -> str | None:
pass
@property
@abstractmethod
def speckle_type(self) -> str:
pass

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