Compare commits

..

16 Commits

Author SHA1 Message Date
KatKatKateryna 0fcc3053ca RenderMaterialProxy 2024-12-06 18:41:48 +00:00
KatKatKateryna 71ca5318b6 fix tests 2024-12-06 03:59:03 +00:00
KatKatKateryna 58b04c246a typo 2024-12-05 20:56:15 +00:00
KatKatKateryna 4ff2931eca unused var 2024-12-05 20:54:15 +00:00
KatKatKateryna 7d25b6b194 isort 2024-12-05 20:52:19 +00:00
KatKatKateryna f33bbbdbba add tests 2024-12-05 20:49:22 +00:00
KatKatKateryna 46b56b9bd3 remove more objects 2024-12-05 20:48:52 +00:00
KatKatKateryna 9358496f49 remove structural classes 2024-12-05 17:05:33 +00:00
KatKatKateryna 6c154b034f typo 2024-12-05 15:49:06 +00:00
KatKatKateryna 0a918ae73a formatting 2024-12-05 15:34:48 +00:00
KatKatKateryna 3ea301f498 remove interface 2024-12-05 15:13:07 +00:00
KatKatKateryna 8e0d47b627 add instances 2024-12-05 15:12:51 +00:00
KatKatKateryna edfb5158f4 add proxies 2024-12-05 15:00:21 +00:00
KatKatKateryna eafa6f2230 add ISpeckleObject model 2024-12-04 18:01:56 +00:00
KatKatKateryna 3362d094ff remove gis classes 2024-12-04 18:01:35 +00:00
KatKatKateryna 21249f0a9a remove gis classes, rename collections 2024-12-02 15:59:08 +00:00
171 changed files with 9307 additions and 7116 deletions
+103 -12
View File
@@ -1,17 +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:
when:
false
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\/.*/
+27
View File
@@ -0,0 +1,27 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/blob/main/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6
ARG VARIANT="3.10"
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="16"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1
USER vscode
RUN curl -sSL https://install.python-poetry.org | python3 -
ENV PATH=$PATH:$HOME/.poetry/env
+55
View File
@@ -0,0 +1,55 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.191.1/containers/python-3
{
"name": "Python 3",
// "build": {
// "dockerfile": "Dockerfile",
// "context": "..",
// "args": {
// // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9
// "VARIANT": "3.6",
// // Options
// "NODE_VERSION": "lts/*"
// }
// },
"dockerComposeFile": "./docker-compose.yaml",
"service": "specklepy",
"workspaceFolder": "/workspaces/specklepy",
"shutdownAction": "stopCompose",
// Set *default* container specific settings.json values on container create.
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.languageServer": "Pylance",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.linting.pylintArgs": [
"--max-line-length=120"
],
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
"python.testing.pytestArgs": [
"tests/",
"-s"
],
"python.testing.pytestEnabled": true,
"editor.formatOnSave": true,
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "poetry config virtualenvs.create false && poetry install",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}
+44
View File
@@ -0,0 +1,44 @@
version: "3.3" # optional since v1.27.0
services:
postgres:
image: cimg/postgres:14.2
environment:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
network_mode: host
redis:
image: cimg/redis:6.2
network_mode: host
speckle-server:
image: speckle/speckle-server:latest
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "localhost"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle2_test"
REDIS_URL: "redis://localhost"
SESSION_SECRET: "keyboard cat"
STRATEGY_LOCAL: "true"
CANONICAL_URL: "http://localhost:3000"
WAIT_HOSTS: localhost:5432, localhost:6379
DISABLE_FILE_UPLOADS: "true"
network_mode: host
specklepy:
build:
dockerfile: Dockerfile
context: .
args:
VARIANT: 3.9
NODE_VERSION: lts/*
volumes:
# Mounts the project folder to '/workspace'. While this file is in .devcontainer,
# mounts are relative to the first file in the list, which is a level up.
- ..:/workspaces/specklepy:cached
# Overrides default command so things don't shut down after the process ends.
command: /bin/sh -c "while sleep 1000; do :; done"
network_mode: host
# networks:
# default:
-54
View File
@@ -1,54 +0,0 @@
name: "Specklepy test and build"
on:
pull_request:
branches:
- "v3-dev"
jobs:
build-and-test:
name: build-and-test
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 --detach --wait
- 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
-98
View File
@@ -1,98 +0,0 @@
name: "Publish Python Package"
on:
push:
branches:
- "v3-dev"
tags:
- "3.*.*"
jobs:
build-and-test:
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
publish-package:
name: "Build and Publish Python Package"
runs-on: ubuntu-latest
needs: build-and-test
# set the environment based on what triggered the workflow
environment:
name: ${{ github.ref_type == 'tag' && 'pypi' || 'testpypi' }}
permissions:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@v5
- name: "Checkout code"
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: "Build package artifacts"
run: uv build
# Logic for TestPyPI (on v3-dev branch push)
- name: "Publish to TestPyPI"
if: ${{ github.ref_type == 'branch' }}
run: uv publish --index test
- name: "Verify TestPyPI package installation"
if: ${{ github.ref_type == 'branch' }}
run: uv run --index test --with specklepy --no-project -- python -c "import specklepy"
# Logic for PyPI (on v3* tag creation)
- name: "Publish to PyPI"
if: ${{ github.ref_type == 'tag' }}
run: uv publish
- name: "Verify PyPI package installation"
if: ${{ github.ref_type == 'tag' }}
run: uv run --with specklepy --no-project -- python -c "import specklepy"
-2
View File
@@ -2,8 +2,6 @@
.envrc
reports/
.volumes/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+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
+7 -7
View File
@@ -25,25 +25,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`
+11 -2
View File
@@ -6,7 +6,7 @@ services:
# Speckle Server dependencies
#######
postgres:
image: "postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf"
image: "postgres:14.5-alpine"
restart: always
environment:
POSTGRES_DB: speckle
@@ -49,6 +49,16 @@ services:
retries: 30
start_period: 10s
####
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend-2:latest
restart: always
ports:
- "0.0.0.0:8080:8080"
speckle-server:
image: speckle/speckle-server:latest
restart: always
@@ -75,7 +85,6 @@ services:
# TODO: Change this to the URL of the speckle server, as accessed from the network
CANONICAL_URL: "http://127.0.0.1:8080"
SPECKLE_AUTOMATE_URL: "http://127.0.0.1:3030"
FRONTEND_ORIGIN: "http://127.0.0.1:8081"
# TODO: Change thvolumes:
REDIS_URL: "redis://redis"
+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
+2040
View File
File diff suppressed because it is too large Load Diff
+61 -72
View File
@@ -1,86 +1,75 @@
[project]
dynamic = ["version"]
# version = "3.0.0a1"
[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",
"ujson>=5.10.0",
]
[dependency-groups]
dev = [
"commitizen>=4.1.0",
"devtools>=0.12.2",
"hatch>=1.14.0",
"hatch-vcs>=0.4.0",
"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 = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
[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-asyncio = "^0.23.0"
pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
devtools = "^0.8.0"
pylint = "^2.14.4"
pydantic-settings = "^2.3.0"
mypy = "^0.982"
pre-commit = "^2.20.0"
commitizen = "^2.38.0"
ruff = "^0.4.4"
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.hatch.version.raw-options]
local_scheme = "no-local-version"
[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.uv.index]]
name = "pypi"
url = "https://pypi.org/simple/"
publish-url = "https://upload.pypi.org/legacy/"
[[tool.uv.index]]
name = "test"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
[tool.isort]
profile = "black"
+48 -65
View File
@@ -1,11 +1,9 @@
# ignoring "line too long" check from linter
# ruff: noqa: E501
"""This module provides an abstraction layer above the Speckle Automate runtime."""
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Tuple, Union
import httpx
from gql import gql
@@ -20,9 +18,7 @@ from speckle_automate.schema import (
)
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.model_inputs import CreateModelInput
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.core.api.models.current import Model, Version
from specklepy.core.api.models import Branch
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.transports.memory import MemoryTransport
@@ -100,26 +96,15 @@ 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
try:
version = self.speckle_client.version.get(
version_id, self.automation_run_data.project_id
)
except SpeckleException as err:
raise ValueError(
f"""\
Could not receive specified version.
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}
"""
) from err
commit = self.speckle_client.commit.get(
self.automation_run_data.project_id, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
base = operations.receive(
version.referenced_object, self._server_transport, self._memory_transport
commit.referencedObject, self._server_transport, self._memory_transport
)
print(
f"It took {self.elapsed():.2f} seconds to receive",
@@ -127,48 +112,45 @@ class AutomationContext:
)
return base
def create_new_model_in_project(
self, model_name: str, model_description: Optional[str] = None
) -> Model:
input = CreateModelInput(
name=model_name,
description=model_description,
project_id=self.automation_run_data.project_id,
)
return self.speckle_client.model.create(input)
def get_model(self, model_id: str) -> Model:
"""
Args:
model_id (str): The id of the model to get
"""
return self.speckle_client.model.get(
model_id, self.automation_run_data.project_id
)
def create_new_version_in_project(
self, root_object: Base, model_id: str, version_message: str = ""
) -> Version:
self, root_object: Base, model_name: str, version_message: str = ""
) -> Tuple[str, str]:
"""Save a base model to a new version on the project.
Args:
root_object (Base): The Speckle base object for the new version.
model_id (str): Id of model to create the new version on.
model_id (str): For now please use a `branchName`!
version_message (str): The message for the new version.
"""
matching_trigger = [
t
for t in self.automation_run_data.triggers
if t.payload.model_id == model_id
]
if matching_trigger:
raise ValueError(
f"The target model: {model_id} cannot match the model"
f" that triggered this automation:"
f" {matching_trigger[0].payload.model_id}"
branch = self.speckle_client.branch.get(
self.automation_run_data.project_id, model_name, 1
)
if isinstance(branch, Branch):
if not branch.id:
raise ValueError("Cannot use the branch without its id")
matching_trigger = [
t
for t in self.automation_run_data.triggers
if t.payload.model_id == branch.id
]
if matching_trigger:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {matching_trigger[0].payload.model_id}"
)
model_id = branch.id
else:
# we just check if it exists
branch_create = self.speckle_client.branch.create(
self.automation_run_data.project_id,
model_name,
)
if isinstance(branch_create, Exception):
raise branch_create
model_id = branch_create
root_object_id = operations.send(
root_object,
@@ -176,17 +158,19 @@ class AutomationContext:
use_default_cache=False,
)
create_version_input = CreateVersionInput(
version_id = self.speckle_client.commit.create(
stream_id=self.automation_run_data.project_id,
object_id=root_object_id,
model_id=model_id,
project_id=self.automation_run_data.project_id,
branch_name=model_name,
message=version_message,
source_application="SpeckleAutomate",
)
version = self.speckle_client.version.create(create_version_input)
self._automation_result.result_versions.append(version.id)
return version
if isinstance(version_id, SpeckleException):
raise version_id
self._automation_result.result_versions.append(version_id)
return model_id, version_id
@property
def context_view(self) -> Optional[str]:
@@ -280,8 +264,7 @@ 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/"
-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
+4 -4
View File
@@ -4,13 +4,13 @@ from enum import Enum
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
from stringcase import camelcase
class AutomateBase(BaseModel):
"""Use this class as a base model for automate related DTO."""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
@@ -39,7 +39,7 @@ class AutomationRunData(BaseModel):
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
@@ -52,7 +52,7 @@ class TestAutomationRunData(BaseModel):
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
+2 -2
View File
@@ -1,3 +1,3 @@
# from specklepy import objects
from specklepy import objects
# __all__ = ["objects"]
__all__ = ["objects"]
+64 -20
View File
@@ -1,4 +1,4 @@
import contextlib
from deprecated import deprecated
from specklepy.api.credentials import Account
from specklepy.api.resources import (
@@ -7,10 +7,15 @@ from specklepy.api.resources import (
OtherUserResource,
ProjectInviteResource,
ProjectResource,
ServerResource,
SubscriptionResource,
VersionResource,
WorkspaceResource,
branch,
commit,
object,
server,
stream,
subscriptions,
user,
)
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
from specklepy.logging import metrics
@@ -29,24 +34,21 @@ class SpeckleClient(CoreSpeckleClient):
```py
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="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)
# create a new project
input = ProjectCreateInput(name="a shiny new project")
project = self.project.create(input)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
# or, use a project id to get an existing project from the server
new_stream = client.project.get("abcdefghij")
# use that stream id to get the stream from the server
new_stream = client.stream.get(id=new_stream_id)
```
"""
@@ -67,14 +69,15 @@ 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()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
@@ -112,18 +115,59 @@ class SpeckleClient(CoreSpeckleClient):
client=self.httpclient,
server_version=server_version,
)
self.workspace = WorkspaceResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.subscription = SubscriptionResource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
# todo: why doesn't this take a server version
)
# Deprecated Resources
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.commit = commit.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.branch = branch.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.object = object.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.subscribe = subscriptions.Resource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
)
@deprecated(
version="2.6.0",
reason=(
"Renamed: please use `authenticate_with_account` or"
" `authenticate_with_token` instead."
),
)
def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments:
token {str} -- an api token
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate_deprecated"}
)
return super().authenticate(token)
def authenticate_with_token(self, token: str) -> None:
"""
+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,
)
+20
View File
@@ -1,15 +1,35 @@
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.models import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
LimitedUser,
Object,
PendingStreamCollaborator,
ServerInfo,
Stream,
Streams,
User,
)
__all__ = [
"Activity",
"ActivityCollection",
"Branch",
"Branches",
"Collaborator",
"Commit",
"Commits",
"LimitedUser",
"Object",
"PendingStreamCollaborator",
"ServerInfo",
"Stream",
"Streams",
"User",
]
+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)
+20 -2
View File
@@ -8,7 +8,17 @@ 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.current.workspace_resource import WorkspaceResource
from specklepy.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
__all__ = [
"ActiveUserResource",
@@ -19,5 +29,13 @@ __all__ = [
"ServerResource",
"SubscriptionResource",
"VersionResource",
"WorkspaceResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
@@ -1,17 +1,20 @@
from typing import List, Optional
from datetime import datetime
from typing import List, Optional, overload
from specklepy.core.api.inputs.user_inputs import (
UserProjectsFilter,
UserUpdateInput,
UserWorkspacesFilter,
)
from deprecated import deprecated
from specklepy.core.api.inputs.project_inputs import UserProjectsFilter
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.models import (
PendingStreamCollaborator,
Project,
ResourceCollection,
User,
)
from specklepy.core.api.models.current import PermissionCheckResult, Workspace
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import ActiveUserResource as CoreResource
from specklepy.logging import metrics
@@ -33,13 +36,42 @@ class ActiveUserResource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Active User Get"})
return super().get()
@deprecated("Use UserUpdateInput overload", version=FE1_DEPRECATION_VERSION)
@overload
def update(
self,
input: UserUpdateInput,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
) -> User:
...
@overload
def update(self, *, input: UserUpdateInput) -> User:
...
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
*,
input: Optional[UserUpdateInput] = None,
) -> User:
metrics.track(metrics.SDK, self.account, {"name": "Active User Update"})
return super().update(input=input)
if isinstance(input, UserUpdateInput):
return super()._update(input=input)
else:
return super()._update(
input=UserUpdateInput(
name=name,
company=company,
bio=bio,
avatar=avatar,
)
)
def get_projects(
self,
@@ -57,25 +89,59 @@ class ActiveUserResource(CoreResource):
)
return super().get_project_invites()
def can_create_personal_projects(self) -> PermissionCheckResult:
metrics.track(
metrics.SDK,
self.account,
{"name": "Active User Can Create Personal Projects Check"},
)
return super().can_create_personal_projects()
def get_workspaces(
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[UserWorkspacesFilter] = None,
) -> ResourceCollection[Workspace]:
metrics.track(metrics.SDK, self.account, {"name": "Active User Get Workspaces"})
return super().get_workspaces(limit, cursor, filter)
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Fetches collection the current authenticated user's activity
as filtered by given parameters
def get_active_workspace(self) -> Optional[Workspace]:
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
Args:
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): Filter results to a single action type.
before (Optional[datetime]): Latest cutoff for activity to include.
after (Optional[datetime]): Oldest cutoff for an activity to include.
cursor (Optional[datetime]): Timestamp cursor for pagination.
Returns:
Activity collection, filtered according to the provided parameters.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
return super().activity(limit, action_type, before, after, cursor)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Fetches all of the current user's pending stream invitations.
Returns:
List[PendingStreamCollaborator]: A list of pending stream invitations.
"""
metrics.track(
metrics.SDK, self.account, {"name": "Active User Get Active Workspace"}
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
)
return super().get_active_workspace()
return super().get_all_pending_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Fetches a specific pending invite for the current user on a given stream.
Args:
stream_id (str): The ID of the stream to look for invites on.
token (Optional[str]): The token of the invite to look for (optional).
Returns:
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
@@ -1,20 +1,27 @@
from typing import Optional
from datetime import datetime
from typing import List, Optional, Union
from deprecated import deprecated
from specklepy.core.api.models import (
ActivityCollection,
LimitedUser,
UserSearchResultCollection,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import OtherUserResource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class OtherUserResource(CoreResource):
"""
Provides API access to other users' profiles and activities on the platform.
This class enables fetching limited information about users,
searching for users by name or email,
and accessing user activity logs with appropriate privacy
and access control measures in place.
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:
@@ -43,3 +50,55 @@ class OtherUserResource(CoreResource):
return super().user_search(
query, limit=limit, cursor=cursor, archived=archived, emailOnly=emailOnly
)
@deprecated(reason="Use user_search instead", version=FE1_DEPRECATION_VERSION)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""
Searches for users by name or email.
The search requires a minimum query length of 3 characters.
Args:
search_query (str): The search string.
limit (int): Maximum number of search results to return.
Returns:
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
query or an exception if the query is too short.
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters."
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
Args:
user_id (str): The ID of the user whose activities are being requested.
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): A specific type of activity to filter.
before (Optional[datetime]): Latest timestamp to include activities before.
after (Optional[datetime]): Earliest timestamp to include activities after.
cursor (Optional[datetime]): Timestamp for pagination cursor.
Returns:
ActivityCollection: A collection of user activities filtered according to specified criteria.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@@ -5,10 +5,8 @@ from specklepy.core.api.inputs.project_inputs import (
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
WorkspaceProjectCreateInput,
)
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
from specklepy.core.api.models.current import ProjectPermissionChecks
from specklepy.core.api.resources import ProjectResource as CoreResource
from specklepy.logging import metrics
@@ -28,12 +26,6 @@ class ProjectResource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Project Get "})
return super().get(project_id)
def get_permissions(self, project_id: str) -> ProjectPermissionChecks:
metrics.track(
metrics.SDK, self.account, {"name": "Project Project Permissions "}
)
return super().get_permissions(project_id)
def get_with_models(
self,
project_id: str,
@@ -58,10 +50,6 @@ class ProjectResource(CoreResource):
metrics.track(metrics.SDK, self.account, {"name": "Project Create"})
return super().create(input)
def create_in_workspace(self, input: WorkspaceProjectCreateInput) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Workspace Project Create"})
return super().create_in_workspace(input)
def update(self, input: ProjectUpdateInput) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Update"})
return super().update(input)
@@ -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:
@@ -42,7 +42,7 @@ class VersionResource(CoreResource):
model_id, project_id, limit=limit, cursor=cursor, filter=filter
)
def create(self, input: CreateVersionInput) -> Version:
def create(self, input: CreateVersionInput) -> str:
metrics.track(metrics.SDK, self.account, {"name": "Version Create"})
return super().create(input)
@@ -1,32 +0,0 @@
from typing import Optional
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
from specklepy.core.api.models.current import Project, ResourceCollection, Workspace
from specklepy.core.api.resources import WorkspaceResource as CoreResource
from specklepy.logging import metrics
class WorkspaceResource(CoreResource):
"""API Access class for workspace"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, workspace_id: str) -> Workspace:
metrics.track(metrics.SDK, self.account, {"name": "Workspace Get"})
return super().get(workspace_id)
def get_projects(
self,
workspace_id: str,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[WorksaceProjectsFilter] = None,
) -> ResourceCollection[Project]:
metrics.track(metrics.SDK, self.account, {"name": "Workspace Get Projects"})
return super().get_projects(workspace_id, limit, cursor, filter)
@@ -0,0 +1,9 @@
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"""
@@ -0,0 +1,108 @@
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.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
"""API Access class for branches"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
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:
"""Create a new branch on this stream
Arguments:
name {str} -- the name of the new branch
description {str} -- a short description of the branch
Returns:
id {str} -- the newly created branch's id
"""
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]:
"""Get a branch by name from a stream
Arguments:
stream_id {str} -- the id of the stream to get the branch from
name {str} -- the name of the branch to get
commits_limit {int} -- maximum number of commits to get
Returns:
Branch -- the fetched branch with its latest commits
"""
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
Arguments:
stream_id {str} -- the id of the stream to get the branches from
branches_limit {int} -- maximum number of branches to get
commits_limit {int} -- maximum number of commits to get
Returns:
List[Branch] -- the branches on the stream
"""
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,
branch_id: str,
name: Optional[str] = None,
description: Optional[str] = None,
):
"""Update a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to update
branch_id {str} -- the id of the branch to update
name {str} -- optional: the updated branch name
description {str} -- optional: the updated branch description
Returns:
bool -- True if update is successful
"""
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
Arguments:
stream_id {str} -- the id of the stream containing the branch to delete
branch_id {str} -- the branch to delete
Returns:
bool -- True if deletion is successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Delete"})
return super().delete(stream_id, branch_id)
@@ -0,0 +1,134 @@
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.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
"""API Access class for commits"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
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
Arguments:
stream_id {str} -- the stream where we can find the commit
commit_id {str} -- the id of the commit you want to get
Returns:
Commit -- the retrieved commit object
"""
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
Arguments:
stream_id {str} -- the stream where the commits are
limit {int} -- the maximum number of commits to fetch (default = 10)
Returns:
List[Commit] -- a list of the most recent commit objects
"""
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,
object_id: str,
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
"""
Creates a commit on a branch
Arguments:
stream_id {str} -- the stream you want to commit to
object_id {str} -- the hash of your commit object
branch_name {str}
-- the name of the branch to commit to (defaults to "main")
message {str}
-- optional: a message to give more information about the commit
source_application{str}
-- optional: the application from which the commit was created
(defaults to "python")
parents {List[str]} -- optional: the id of the parent commits
Returns:
str -- the id of the created commit
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Create"})
return super().create(
stream_id, object_id, branch_name, message, source_application, parents
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
Update a commit
Arguments:
stream_id {str}
-- the id of the stream that contains the commit you'd like to update
commit_id {str} -- the id of the commit you'd like to update
message {str} -- the updated commit message
Returns:
bool -- True if the operation succeeded
"""
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
Arguments:
stream_id {str}
-- the id of the stream that contains the commit you'd like to delete
commit_id {str} -- the id of the commit you'd like to delete
Returns:
bool -- True if the operation succeeded
"""
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,
commit_id: str,
source_application: str = "python",
message: Optional[str] = None,
) -> bool:
"""
Mark a commit object a received by the source application.
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Received"})
return super().received(stream_id, commit_id, source_application, message)
@@ -0,0 +1,63 @@
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.logging import metrics
from specklepy.objects.base import Base
class Resource(CoreResource):
"""API Access class for objects"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
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
Arguments:
stream_id {str} -- the id of the stream for the object
object_id {str} -- the hash of the object you want to get
Returns:
Base -- the returned Base object
"""
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()`.
Create a new object on a stream.
To send a base object, you can prepare it by running it through the
`BaseObjectSerializer.traverse_base()` function to get a valid (serialisable)
object to send.
NOTE: this does not create a commit - you can create one with
`SpeckleClient.commit.create`.
Dynamic fields will be located in the 'data' dict of the received `Base` object
Arguments:
stream_id {str} -- the id of the stream you want to send the object to
objects {List[Dict]}
-- a list of base dictionary objects (NOTE: must be json serialisable)
Returns:
str -- the id of the object
"""
metrics.track(metrics.SDK, self.account, {"name": "Object Create"})
return super().create(stream_id, objects)
@@ -0,0 +1,11 @@
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
"""
@@ -0,0 +1,9 @@
from deprecated import deprecated
from specklepy.api.resources import ServerResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""Renamed to ServerResource"""
@@ -0,0 +1,322 @@
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.logging import metrics
class Resource(CoreResource):
"""API Access class for streams"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
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
Arguments:
id {str} -- the stream id
branch_limit {int} -- the maximum number of branches to return
commit_limit {int} -- the maximum number of commits to return
Returns:
Stream -- the retrieved stream
"""
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
Arguments:
stream_limit {int} -- The maximum number of streams to return
Returns:
List[Stream] -- A list of Stream objects
"""
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",
description: str = "No description provided",
is_public: bool = True,
) -> str:
"""Create a new stream
Arguments:
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
id {str} -- the id of the newly created stream
"""
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,
name: Optional[str] = None,
description: Optional[str] = None,
is_public: Optional[bool] = None,
) -> bool:
"""Update an existing stream
Arguments:
id {str} -- the id of the stream to be updated
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
bool -- whether the stream update was successful
"""
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
Arguments:
id {str} -- the id of the stream to delete
Returns:
bool -- whether the deletion was successful
"""
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,
limit: int = 25,
branch_limit: int = 10,
commit_limit: int = 10,
):
"""Search for streams by name, description, or id
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
branch_limit {int} -- the maximum number of branches to return
commit_limit {int} -- the maximum number of commits to return
Returns:
List[Stream] -- a list of Streams that match the search query
"""
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.
Arguments:
stream_id {str} -- the id of the stream to favorite / unfavorite
favorited {bool}
-- whether to favorite (True) or unfavorite (False) the stream
Returns:
Stream -- the stream with its `id`, `name`, and `favoritedDate`
"""
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]:
"""Get all of the pending invites on a stream.
You must be a `stream:owner` to query this.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the stream id from which to get the pending invites
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the specified stream
"""
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,
email: Optional[str] = None,
user_id: Optional[str] = None,
role: str = "stream:contributor", # should default be reviewer?
message: Optional[str] = None,
):
"""Invite someone to a stream using either their email or user id
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
email {str} -- the email of the user to invite (use this OR `user_id`)
user_id {str} -- the id of the user to invite (use this OR `email`)
role {str}
-- the role to assign to the user (defaults to `stream:contributor`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
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,
emails: Optional[List[str]] = None,
user_ids: Optional[List[None]] = None,
message: Optional[str] = None,
) -> bool:
"""Invite a batch of users to a specified stream.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
emails {List[str]}
-- the email of the user to invite (use this and/or `user_ids`)
user_id {List[str]}
-- the id of the user to invite (use this and/or `emails`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
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
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream invite
invite_id {str} -- the id of the invite to use
Returns:
bool -- true if the operation was successful
"""
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
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str}
-- the id of the stream for which the user has a pending invite
token {str} -- the token of the invite to use
accept {bool} -- whether or not to accept the invite (defaults to True)
Returns:
bool -- true if the operation was successful
"""
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
Valid for Speckle Server >=2.6.4
Arguments:
stream_id {str} -- the id of the stream to grant permissions to
user_id {str} -- the id of the user to grant permissions for
role {str} -- the role to grant the user
Returns:
bool -- True if the operation was successful
"""
metrics.track(
metrics.SDK,
self.account,
{"name": "Stream Permission Update", "role": role},
)
return super().update_permission(stream_id, user_id, role)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
Arguments:
stream_id {str} -- the id of the stream to revoke permissions from
user_id {str} -- the id of the user to revoke permissions from
Returns:
bool -- True if the operation was successful
"""
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,
action_type: Optional[str] = None,
limit: int = 20,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of any tz
as they will be converted to UTC ISO format strings
stream_id {str} -- the id of the stream to get activity from
action_type {str}
-- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime}
-- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Activity"})
return super().activity(stream_id, action_type, limit, before, after, cursor)
@@ -0,0 +1,107 @@
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.logging import metrics
class Resource(CoreResource):
"""API Access class for subscriptions"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
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.
Use this to display an up-to-date list of streams.
Arguments:
callback {Callable[Stream]} -- a function that takes the updated stream
as an argument and executes each time a stream is added
Returns:
Stream -- the update stream
"""
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):
"""
Subscribes to stream updated event.
Use this in clients/components that pertain only to this stream.
Arguments:
id {str} -- the stream id of the stream to subscribe to
callback {Callable[Stream]}
-- a function that takes the updated stream
as an argument and executes each time the stream is updated
Returns:
Stream -- the update stream
"""
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Updated"}
)
return super().stream_updated(id, callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_removed(self, callback: Optional[Callable] = None):
"""Subscribes to stream removed event for your profile.
Use this to display an up-to-date list of streams for your profile.
NOTE: If someone revokes your permissions on a stream,
this subscription will be triggered with an extra value of revokedBy
in the payload.
Arguments:
callback {Callable[Dict]}
-- a function that takes the returned dict as an argument
and executes each time a stream is removed
Returns:
dict -- dict containing 'id' of stream removed and optionally 'revokedBy'
"""
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Removed"}
)
return super().stream_removed(callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def subscribe(
self,
query: DocumentNode,
params: Optional[Dict] = None,
callback: Optional[Callable] = None,
return_type: Optional[Union[str, List]] = None,
schema=None,
parse_response: bool = True,
):
# if self.client.transport.websocket is None:
# TODO: add multiple subs to the same ws connection
async with self.client as session:
async for res in session.subscribe(query, variable_values=params):
res = self._step_into_response(response=res, return_type=return_type)
if parse_response:
res = self._parse_response(response=res, schema=schema)
if callback is not None:
callback(res)
else:
return res
@@ -0,0 +1,153 @@
from datetime import datetime
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.logging import metrics
from specklepy.logging.exceptions import SpeckleException
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = (
"The user resource is deprecated, please use the active_user or other_user"
" resources"
)
class Resource(CoreResource):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = User
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get(self, id: Optional[str] = None) -> User:
"""
Gets the profile of a user.
If no id argument is provided, will return the current authenticated
user's profile (as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
metrics.track(metrics.SDK, self.account, {"name": "User Get_deprecated"})
return super().get(id)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[User], SpeckleException]:
"""
Searches for user by name or email.
The search query must be at least 3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[User] -- a list of User objects that match the search query
"""
metrics.track(metrics.SDK, self.account, {"name": "User Search_deprecated"})
return super().search(search_query, limit)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns:
bool -- True if your profile was updated successfully
"""
# metrics.track(metrics.USER, self.account, {"name": "update"})
metrics.track(metrics.SDK, self.account, {"name": "User Update_deprecated"})
return super().update(name, company, bio, avatar)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def activity(
self,
user_id: Optional[str] = None,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated
user's activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as
they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime}
-- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
metrics.track(metrics.SDK, self.account, {"name": "User Activity_deprecated"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(
metrics.SDK, self.account, {"name": "User GetAllInvites_deprecated"}
)
return super().get_all_pending_invites()
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(metrics.SDK, self.account, {"name": "User GetInvite_deprecated"})
return super().get_pending_invite(stream_id, token)
+3 -3
View File
@@ -10,7 +10,7 @@ class StreamWrapper(CoreStreamWrapper):
The `StreamWrapper` gives you some handy helpers to deal with urls and
get authenticated clients and transports.
Construct a `StreamWrapper` with a URL of a model, version, or object.
Construct a `StreamWrapper` with a stream, branch, commit, or object URL.
The corresponding ids will be stored
in the wrapper. If you have local accounts on the machine,
you can use the `get_account` and `get_client` methods
@@ -21,8 +21,8 @@ class StreamWrapper(CoreStreamWrapper):
```py
from specklepy.api.wrapper import StreamWrapper
# provide a url for a model, version, or object
wrapper = StreamWrapper("https://app.speckle.systems/projects/3073b96e86/models/0fe47c9dca@604bea8cc6")
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
+76 -24
View File
@@ -1,14 +1,15 @@
import contextlib
import re
from typing import Dict
from warnings import warn
from deprecated import deprecated
from gql import Client
from gql.transport.exceptions import TransportServerError
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.websockets import WebsocketsTransport
from specklepy.core.api.credentials import Account
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,
@@ -18,7 +19,12 @@ from specklepy.core.api.resources import (
ServerResource,
SubscriptionResource,
VersionResource,
WorkspaceResource,
branch,
commit,
object,
stream,
subscriptions,
user,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
@@ -37,24 +43,21 @@ class SpeckleClient:
```py
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="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)
# create a new project
input = ProjectCreateInput(name="a shiny new project")
project = self.project.create(input)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
# or, use a project id to get an existing project from the server
new_stream = client.project.get("abcdefghij")
# use that stream id to get the stream from the server
new_stream = client.stream.get(id=new_stream_id)
```
"""
@@ -99,8 +102,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):
@@ -118,6 +120,23 @@ class SpeckleClient:
f" {self.account.token is not None} )"
)
@deprecated(
version="2.6.0",
reason=(
"Renamed: please use `authenticate_with_account` or"
" `authenticate_with_token` instead."
),
)
def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments:
token {str} -- an api token
"""
self.authenticate_with_account(get_account_from_token(token))
def authenticate_with_token(self, token: str) -> None:
"""
Authenticate the client using a personal access token.
@@ -168,10 +187,9 @@ class SpeckleClient:
if ex.exception.code == 403:
warn(
SpeckleWarning(
"Possibly invalid token - could not authenticate "
f"Speckle Client for server {self.url}"
),
stacklevel=2,
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
)
)
else:
raise ex
@@ -185,8 +203,10 @@ class SpeckleClient:
)
server_version = None
with contextlib.suppress(Exception):
try:
server_version = self.server.version()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
@@ -224,14 +244,46 @@ class SpeckleClient:
client=self.httpclient,
server_version=server_version,
)
self.workspace = WorkspaceResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.subscription = SubscriptionResource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
)
# Deprecated Resources
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.commit = commit.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.branch = branch.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.object = object.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.subscribe = subscriptions.Resource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
)
def __getattr__(self, name):
try:
attr = getattr(resources, name)
return attr.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
except AttributeError:
raise SpeckleException(
f"Method {name} is not supported by the SpeckleClient class"
)
+1 -1
View File
@@ -150,7 +150,7 @@ def get_accounts_for_server(host: str) -> List[Account]:
for acc in all_accounts:
moved_from = (
acc.serverInfo.migration.moved_from if acc.serverInfo.migration else None
acc.serverInfo.migration.movedFrom if acc.serverInfo.migration else None
)
if moved_from and host == urlparse(moved_from).netloc:
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
class ProjectVisibility(str, Enum):
PRIVATE = "PRIVATE"
PUBLIC = "PUBLIC"
UNLISTED = "UNLISTED"
UNLISTEd = "UNLISTED"
class UserProjectsUpdatedMessageType(str, Enum):
-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",
]
+10 -10
View File
@@ -1,26 +1,26 @@
from typing import Optional, Sequence
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
from pydantic import BaseModel
class CreateModelInput(GraphQLBaseModel):
class CreateModelInput(BaseModel):
name: str
description: Optional[str] = None
project_id: str
projectId: str
class DeleteModelInput(GraphQLBaseModel):
class DeleteModelInput(BaseModel):
id: str
project_id: str
projectId: str
class UpdateModelInput(GraphQLBaseModel):
class UpdateModelInput(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
project_id: str
projectId: str
class ModelVersionsFilter(GraphQLBaseModel):
priority_ids: Sequence[str]
priority_ids_only: Optional[bool] = None
class ModelVersionsFilter(BaseModel):
priorityIds: Sequence[str]
priorityIdsOnly: Optional[bool] = None
+19 -29
View File
@@ -1,62 +1,52 @@
from typing import Optional, Sequence
from pydantic import BaseModel
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
class ProjectCreateInput(GraphQLBaseModel):
class ProjectCreateInput(BaseModel):
name: Optional[str]
description: Optional[str]
visibility: Optional[ProjectVisibility]
class WorkspaceProjectCreateInput(GraphQLBaseModel):
name: Optional[str]
description: Optional[str]
visibility: Optional[ProjectVisibility]
workspaceId: str
class ProjectInviteCreateInput(GraphQLBaseModel):
class ProjectInviteCreateInput(BaseModel):
email: Optional[str]
role: Optional[str]
server_role: Optional[str]
serverRole: Optional[str]
userId: Optional[str]
class ProjectInviteUseInput(GraphQLBaseModel):
class ProjectInviteUseInput(BaseModel):
accept: bool
project_id: str
projectId: str
token: str
class ProjectModelsFilter(GraphQLBaseModel):
class ProjectModelsFilter(BaseModel):
contributors: Optional[Sequence[str]] = None
exclude_ids: Optional[Sequence[str]] = None
excludeIds: Optional[Sequence[str]] = None
ids: Optional[Sequence[str]] = None
only_with_versions: Optional[bool] = None
onlyWithVersions: Optional[bool] = None
search: Optional[str] = None
source_apps: Optional[Sequence[str]] = None
sourceApps: Optional[Sequence[str]] = None
class ProjectUpdateInput(GraphQLBaseModel):
class ProjectUpdateInput(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
allow_public_comments: Optional[bool] = None
allowPublicComments: Optional[bool] = None
visibility: Optional[ProjectVisibility] = None
class ProjectUpdateRoleInput(GraphQLBaseModel):
user_id: str
project_id: str
class ProjectUpdateRoleInput(BaseModel):
userId: str
projectId: str
role: Optional[str]
class WorksaceProjectsFilter(GraphQLBaseModel):
search: Optional[str]
"""Filter out projects by name"""
with_project_role_only: Optional[bool]
"""
Only return workspace projects that the active user has an explicit project role in
"""
class UserProjectsFilter(BaseModel):
search: str
onlyWithRole: Optional[Sequence[str]] = None
+3 -15
View File
@@ -1,22 +1,10 @@
from typing import Optional, Sequence
from typing import Optional
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
from pydantic import BaseModel
class UserUpdateInput(GraphQLBaseModel):
class UserUpdateInput(BaseModel):
avatar: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
name: Optional[str] = None
class UserProjectsFilter(GraphQLBaseModel):
search: Optional[str] = None
only_with_roles: Optional[Sequence[str]] = None
workspace_id: Optional[str] = None
personal_only: Optional[bool] = None
include_implicit_access: Optional[bool] = None
class UserWorkspacesFilter(GraphQLBaseModel):
search: Optional[str]
+21 -21
View File
@@ -1,37 +1,37 @@
from typing import Optional, Sequence
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
from pydantic import BaseModel
class UpdateVersionInput(GraphQLBaseModel):
version_id: str
project_id: str
class UpdateVersionInput(BaseModel):
versionId: str
projectId: str
message: Optional[str]
class MoveVersionsInput(GraphQLBaseModel):
target_model_name: str
version_ids: Sequence[str]
project_id: str
class MoveVersionsInput(BaseModel):
targetModelName: str
versionIds: Sequence[str]
projectId: str
class DeleteVersionsInput(GraphQLBaseModel):
version_ids: Sequence[str]
project_id: str
class DeleteVersionsInput(BaseModel):
versionIds: Sequence[str]
projectId: str
class CreateVersionInput(GraphQLBaseModel):
object_id: str
model_id: str
project_id: str
class CreateVersionInput(BaseModel):
objectId: str
modelId: str
projectId: str
message: Optional[str] = None
source_application: Optional[str] = "py"
total_children_count: Optional[int] = None
sourceApplication: Optional[str] = "py"
totalChildrenCount: Optional[int] = None
parents: Optional[Sequence[str]] = None
class MarkReceivedVersionInput(GraphQLBaseModel):
version_id: str
project_id: str
source_application: str
class MarkReceivedVersionInput(BaseModel):
versionId: str
projectId: str
sourceApplication: str
message: Optional[str] = None
+28
View File
@@ -17,6 +17,20 @@ from specklepy.core.api.models.current import (
UserSearchResultCollection,
Version,
)
from specklepy.core.api.models.deprecated import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
Object,
Stream,
Streams,
)
from specklepy.core.api.models.instances import InstanceDefinitionProxy, InstanceProxy
from specklepy.core.api.models.proxies import ColorProxy, GroupProxy
from specklepy.core.api.models.subscription_messages import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
@@ -46,4 +60,18 @@ __all__ = [
"ProjectModelsUpdatedMessage",
"ProjectUpdatedMessage",
"ProjectVersionsUpdatedMessage",
"Collaborator",
"Commit",
"Commits",
"Object",
"Branch",
"Branches",
"Stream",
"Streams",
"Activity",
"ActivityCollection",
"InstanceProxy",
"InstanceDefinitionProxy",
"ColorProxy",
"GroupProxy",
]
+54 -104
View File
@@ -1,14 +1,15 @@
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.graphql_base_model import GraphQLBaseModel
from specklepy.logging.exceptions import WorkspacePermissionException
from specklepy.core.api.models.deprecated import Streams
T = TypeVar("T")
class User(GraphQLBaseModel):
class User(BaseModel):
id: str
email: Optional[str] = None
name: str
@@ -17,6 +18,7 @@ class User(GraphQLBaseModel):
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
streams: Optional["Streams"] = None
def __repr__(self):
return (
@@ -28,18 +30,18 @@ class User(GraphQLBaseModel):
return self.__repr__()
class ResourceCollection(GraphQLBaseModel, Generic[T]):
total_count: int
class ResourceCollection(BaseModel, Generic[T]):
totalCount: int
items: List[T]
cursor: Optional[str] = None
class ServerMigration(GraphQLBaseModel):
moved_from: Optional[str]
moved_to: Optional[str]
class ServerMigration(BaseModel):
movedFrom: Optional[str]
movedTo: Optional[str]
class AuthStrategy(GraphQLBaseModel):
class AuthStrategy(BaseModel):
color: Optional[str]
icon: str
id: str
@@ -47,34 +49,31 @@ class AuthStrategy(GraphQLBaseModel):
url: str
class ServerConfiguration(GraphQLBaseModel):
blob_size_limit_bytes: int
object_multipart_upload_size_limit_bytes: int
object_size_limit_bytes: int
class ServerConfiguration(BaseModel):
blobSizeLimitBytes: int
objectMultipartUploadSizeLimitBytes: int
objectSizeLimitBytes: int
class ServerWorkspacesInfo(GraphQLBaseModel):
workspaces_enabled: bool
# Keeping this one all Optionals at the minute,
# because its used both as a deserialization model for GQL and Account Management
class ServerInfo(GraphQLBaseModel):
# 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
admin_contact: Optional[str] = None
adminContact: Optional[str] = None
description: Optional[str] = None
canonical_url: Optional[str] = None
canonicalUrl: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
auth_strategies: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
workspaces: Optional[ServerWorkspacesInfo] = None
# TODO separate gql model from account management model
class LimitedUser(GraphQLBaseModel):
class LimitedUser(BaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
@@ -85,34 +84,24 @@ class LimitedUser(GraphQLBaseModel):
verified: Optional[bool]
role: Optional[str]
def __repr__(self):
return (
f"(name: {self.name}, "
f"id: {self.id}, "
f"bio: {self.bio}, "
f"company: {self.company}, "
f"verified: {self.verified}, "
f"role: {self.role})"
)
class PendingStreamCollaborator(GraphQLBaseModel):
class PendingStreamCollaborator(BaseModel):
id: str
invite_id: str
stream_id: Optional[str] = None
inviteId: str
streamId: Optional[str] = None
projectId: str
stream_name: Optional[str] = None
project_name: str
streamName: Optional[str] = None
projectName: str
title: str
role: str
invited_by: LimitedUser
invitedBy: LimitedUser
user: Optional[LimitedUser] = None
token: Optional[str]
def __repr__(self):
return (
f"PendingStreamCollaborator( inviteId: {self.invite_id}, streamId:"
f" {self.stream_id}, role: {self.role}, title: {self.title}, invitedBy:"
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})"
)
@@ -120,54 +109,48 @@ class PendingStreamCollaborator(GraphQLBaseModel):
return self.__repr__()
class ProjectCollaborator(GraphQLBaseModel):
class ProjectCollaborator(BaseModel):
id: str
role: str
user: LimitedUser
class Version(GraphQLBaseModel):
author_user: Optional[LimitedUser]
created_at: datetime
class Version(BaseModel):
authorUser: Optional[LimitedUser]
createdAt: datetime
id: str
message: Optional[str]
preview_url: str
referenced_object: Optional[str]
"""Maybe null if workspaces version history limit has been exceeded"""
source_application: Optional[str]
previewUrl: str
referencedObject: str
sourceApplication: Optional[str]
class Model(GraphQLBaseModel):
author: Optional[LimitedUser]
created_at: datetime
class Model(BaseModel):
author: LimitedUser
createdAt: datetime
description: Optional[str]
display_name: str
displayName: str
id: str
name: str
preview_url: Optional[str]
updated_at: datetime
previewUrl: Optional[str]
updatedAt: datetime
class ModelWithVersions(Model):
versions: ResourceCollection[Version]
class ProjectPermissionChecks(GraphQLBaseModel):
can_create_model: "PermissionCheckResult"
can_delete: "PermissionCheckResult"
class Project(GraphQLBaseModel):
allow_public_comments: bool
created_at: datetime
class Project(BaseModel):
allowPublicComments: bool
createdAt: datetime
description: Optional[str]
id: str
name: str
role: Optional[str]
source_apps: List[str]
updated_at: datetime
sourceApps: List[str]
updatedAt: datetime
visibility: ProjectVisibility
workspace_id: Optional[str]
workspaceId: Optional[str]
class ProjectWithModels(Project):
@@ -175,47 +158,14 @@ class ProjectWithModels(Project):
class ProjectWithTeam(Project):
invited_team: List[PendingStreamCollaborator]
invitedTeam: List[PendingStreamCollaborator]
team: List[ProjectCollaborator]
class ProjectCommentCollection(ResourceCollection[T], Generic[T]):
total_archived_count: int
totalArchivedCount: int
class UserSearchResultCollection(GraphQLBaseModel):
class UserSearchResultCollection(BaseModel):
items: List[LimitedUser]
cursor: Optional[str] = None
class PermissionCheckResult(GraphQLBaseModel):
authorized: bool
code: str
message: str
def ensure_authorised(self) -> None:
"""Raises WorkspacePermissionException if not authorized"""
if not self.authorized:
raise WorkspacePermissionException(self.message)
class WorkspacePermissionChecks(GraphQLBaseModel):
can_create_project: PermissionCheckResult
class WorkspaceCreationState(GraphQLBaseModel):
completed: bool
class Workspace(GraphQLBaseModel):
id: str
name: str
role: Optional[str]
slug: str
logo: Optional[str]
created_at: datetime
updated_at: datetime
read_only: bool
description: Optional[str]
creation_state: Optional[WorkspaceCreationState]
permissions: WorkspacePermissionChecks
+144
View File
@@ -0,0 +1,144 @@
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
role: Optional[str] = None
avatar: Optional[str] = None
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Commit(BaseModel):
id: Optional[str] = None
message: Optional[str] = None
authorName: Optional[str] = None
authorId: Optional[str] = None
authorAvatar: Optional[str] = None
branchName: Optional[str] = None
createdAt: Optional[datetime] = None
sourceApplication: Optional[str] = None
referencedObject: Optional[str] = None
totalChildrenCount: Optional[int] = None
parents: Optional[List[str]] = None
def __repr__(self) -> str:
return (
f"Commit( id: {self.id}, message: {self.message}, referencedObject:"
f" {self.referencedObject}, authorName: {self.authorName}, branchName:"
f" {self.branchName}, createdAt: {self.createdAt} )"
)
def __str__(self) -> str:
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
applicationId: Optional[str] = None
totalChildrenCount: Optional[int] = None
createdAt: Optional[datetime] = None
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class Branch(BaseModel):
id: Optional[str] = None
name: Optional[str] = None
description: Optional[str] = None
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
role: Optional[str] = None
isPublic: Optional[bool] = None
description: Optional[str] = None
createdAt: Optional[datetime] = None
updatedAt: Optional[datetime] = None
collaborators: List[Collaborator] = Field(default_factory=list)
branches: Optional[Branches] = None
commit: Optional[Commit] = None
object: Optional[Object] = None
commentCount: Optional[int] = None
favoritedDate: Optional[datetime] = None
favoritesCount: Optional[int] = None
def __repr__(self):
return (
f"Stream( id: {self.id}, name: {self.name}, description:"
f" {self.description}, isPublic: {self.isPublic})"
)
def __str__(self) -> str:
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 Activity(BaseModel):
actionType: Optional[str] = None
info: Optional[dict] = None
userId: Optional[str] = None
streamId: Optional[str] = None
resourceId: Optional[str] = None
resourceType: Optional[str] = None
message: Optional[str] = None
time: Optional[datetime] = None
def __repr__(self) -> str:
return (
f"Activity( streamId: {self.streamId}, actionType: {self.actionType},"
f" message: {self.message}, userId: {self.userId} )"
)
def __str__(self) -> str:
return self.__repr__()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
class ActivityCollection(BaseModel):
totalCount: Optional[int] = None
items: Optional[List[Activity]] = None
cursor: Optional[datetime] = None
def __repr__(self) -> str:
return (
f"ActivityCollection( totalCount: {self.totalCount}, items:"
f" {len(self.items) if self.items else 0}, cursor:"
f" {self.cursor.isoformat() if self.cursor else None} )"
)
def __str__(self) -> str:
return self.__repr__()
@@ -1,17 +0,0 @@
from pydantic import AliasGenerator, BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class GraphQLBaseModel(BaseModel):
"""
Parent class for all GraphQL Object Model classes
Sets-up a pydantic config to serialize properties using a camel case alias
"""
model_config = ConfigDict(
alias_generator=AliasGenerator(
serialization_alias=to_camel,
validation_alias=to_camel,
),
populate_by_name=True,
)
@@ -0,0 +1,28 @@
from specklepy.objects.base import Base
class InstanceProxy(
Base,
speckle_type="Speckle.Core.Models.Instances.InstanceProxy",
):
"""
A proxy class for an instance (e.g, a rhino block, or an autocad block reference).
"""
definitionId: str
transform: list[float]
units: str
maxDepth: int
class InstanceDefinitionProxy(
Base,
speckle_type="Speckle.Core.Models.Instances.InstanceDefinitionProxy",
):
"""
A proxy class for an instance definition.
"""
objects: list[str]
maxDepth: int
name: str
+27
View File
@@ -0,0 +1,27 @@
from specklepy.objects.base import Base
class ColorProxy(
Base,
speckle_type="Speckle.Core.Models.Proxies.ColorProxy",
):
"""
Represents a color that is found on objects and collections in a root collection.
"""
objects: list[str]
value: int
name: str | None # nullable but required
class GroupProxy(
Base,
speckle_type="Speckle.Core.Models.Proxies.GroupProxy",
):
"""
Grouped objects with a meaningful way for host application so use this proxy if you want to group object references for any purpose.
i.e. in rhino -> creating group make objects selectable/moveable/editable together.
"""
objects: list[str]
name: str
@@ -1,5 +1,7 @@
from typing import Optional
from pydantic import BaseModel
from specklepy.core.api.enums import (
ProjectModelsUpdatedMessageType,
ProjectUpdatedMessageType,
@@ -7,29 +9,28 @@ from specklepy.core.api.enums import (
UserProjectsUpdatedMessageType,
)
from specklepy.core.api.models.current import Model, Project, Version
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
class UserProjectsUpdatedMessage(GraphQLBaseModel):
class UserProjectsUpdatedMessage(BaseModel):
id: str
type: UserProjectsUpdatedMessageType
project: Optional[Project]
class ProjectModelsUpdatedMessage(GraphQLBaseModel):
class ProjectModelsUpdatedMessage(BaseModel):
id: str
type: ProjectModelsUpdatedMessageType
model: Optional[Model]
class ProjectUpdatedMessage(GraphQLBaseModel):
class ProjectUpdatedMessage(BaseModel):
id: str
type: ProjectUpdatedMessageType
project: Optional[Project]
class ProjectVersionsUpdatedMessage(GraphQLBaseModel):
class ProjectVersionsUpdatedMessage(BaseModel):
id: str
type: ProjectVersionsUpdatedMessageType
model_id: str
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 -3
View File
@@ -18,7 +18,7 @@ from specklepy.transports.sqlite import SQLiteTransport
T = TypeVar("T", bound=BaseModel)
class ResourceBase:
class ResourceBase(object):
def __init__(
self,
account: Account,
@@ -101,8 +101,7 @@ class ResourceBase:
parse_response: bool = True,
) -> Any:
"""Executes the GraphQL query"""
# This method has quite complex and ambiguous typing,
# and counter-intuitive error handling
# 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:
+20 -2
View File
@@ -10,7 +10,17 @@ 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.current.workspace_resource import WorkspaceResource
from specklepy.core.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
__all__ = [
"ActiveUserResource",
@@ -21,5 +31,13 @@ __all__ = [
"ServerResource",
"SubscriptionResource",
"VersionResource",
"WorkspaceResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
@@ -1,19 +1,22 @@
from typing import List, Optional
from datetime import datetime, timezone
from typing import List, Optional, overload
from deprecated import deprecated
from gql import gql
from specklepy.core.api.inputs.user_inputs import (
UserProjectsFilter,
UserUpdateInput,
UserWorkspacesFilter,
)
from specklepy.core.api.inputs.project_inputs import UserProjectsFilter
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Project,
ResourceCollection,
User,
)
from specklepy.core.api.models.current import PermissionCheckResult, Workspace
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
@@ -35,12 +38,10 @@ class ActiveUserResource(ResourceBase):
self.schema = User
def get(self) -> Optional[User]:
"""Gets the currently active user profile
(as extracted from the authorization header)
"""Gets the currently active user profile (as extracted from the authorization header)
Returns:
User -- the requested user, or none if no authentication token
is provided to the Client
User -- the requested user, or none if no authentication token is provided to the Client
"""
QUERY = gql(
"""
@@ -65,7 +66,7 @@ class ActiveUserResource(ResourceBase):
DataResponse[Optional[User]], QUERY, variables
).data
def update(self, input: UserUpdateInput) -> User:
def _update(self, input: UserUpdateInput) -> User:
QUERY = gql(
"""
mutation ActiveUserMutations($input: UserUpdateInput!) {
@@ -85,12 +86,48 @@ class ActiveUserResource(ResourceBase):
"""
)
variables = {"input": input.model_dump(warnings="error", by_alias=True)}
variables = {"input": input.model_dump(warnings="error")}
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:
...
def update(
self,
name: Optional[str] = None,
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,
)
)
def get_projects(
self,
*,
@@ -126,9 +163,7 @@ class ActiveUserResource(ResourceBase):
variables = {
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error", by_alias=True)
if filter
else None,
"filter": filter.model_dump(warnings="error") if filter else None,
}
response = self.make_request_and_parse_response(
@@ -196,145 +231,183 @@ class ActiveUserResource(ResourceBase):
return response.data.data
def can_create_personal_projects(self) -> PermissionCheckResult:
QUERY = gql(
"""
query CanCreatePersonalProject {
data:activeUser {
data:permissions {
data:canCreatePersonalProject {
authorized
code
message
}
}
}
}
"""
)
response = self.make_request_and_parse_response(
DataResponse[Optional[DataResponse[DataResponse[PermissionCheckResult]]]],
QUERY,
)
if response.data is None:
raise GraphQLException(
"GraphQL response indicated that the ActiveUser could not be found"
)
return response.data.data.data
def get_workspaces(
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[UserWorkspacesFilter] = None,
) -> ResourceCollection[Workspace]:
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
This feature is only available on Workspace enabled servers (server versions
>=2.23.17) e.g. app.speckle.systems
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's
activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
QUERY = gql(
query = gql(
"""
query ActiveUser($limit: Int!, $cursor: String, $filter: UserWorkspacesFilter) {
data:activeUser {
data:workspaces(limit: $limit, cursor: $cursor, filter: $filter) {
cursor
totalCount
items {
id
name
role
slug
logo
createdAt
updatedAt
readOnly
description
creationState
{
completed
query UserActivity(
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
activeUser {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
permissions {
canCreateProject {
authorized
code
message
}
}
}
}
}
}
""" # noqa: E501
"""
)
variables = {
params = {
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error", by_alias=True)
if filter
else None,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
response = self.make_request_and_parse_response(
DataResponse[Optional[DataResponse[ResourceCollection[Workspace]]]],
QUERY,
variables,
return self.make_request(
query=query,
params=params,
return_type=["activeUser", "activity"],
schema=ActivityCollection,
)
if response.data is None:
raise GraphQLException(
"GraphQL response indicated that the ActiveUser could not be found"
)
@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
return response.data.data
Requires Speckle Server version >= 2.6.4
def get_active_workspace(self) -> Optional[Workspace]:
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
This feature is only available on Workspace enabled servers (server versions
>=2.23.17) e.g. app.speckle.systems
"""
QUERY = gql(
self._check_invites_supported()
query = gql(
"""
query ActiveUser {
data:activeUser {
data:activeWorkspace {
id
name
role
slug
logo
createdAt
updatedAt
readOnly
description
creationState
{
completed
}
permissions {
canCreateProject {
authorized
code
message
query StreamInvites {
streamInvites{
id
token
inviteId
streamId
streamName
projectId
projectName
title
role
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
}
}
""" # noqa: E501
"""
)
response = self.make_request_and_parse_response(
DataResponse[Optional[DataResponse[Optional[Workspace]]]],
QUERY,
return self.make_request(
query=query,
return_type="streamInvites",
schema=PendingStreamCollaborator,
)
if response.data is None:
raise GraphQLException(
"GraphQL response indicated that the ActiveUser could not be found"
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
return response.data.data
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
self._check_invites_supported()
query = gql(
"""
query StreamInvite($streamId: String!, $token: String) {
streamInvite(streamId: $streamId, token: $token) {
id
token
inviteId
streamId
streamName
projectId
projectName
title
role
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
"""
)
params = {"streamId": stream_id}
if token:
params["token"] = token
return self.make_request(
query=query,
params=params,
return_type="streamInvite",
schema=PendingStreamCollaborator,
)
@@ -76,24 +76,14 @@ class ModelResource(ResourceBase):
) -> ModelWithVersions:
QUERY = gql(
"""
query ModelGetWithVersions(
$modelId: String!,
$projectId: String!,
$versionsLimit: Int!,
$versionsCursor: String,
$versionsFilter: ModelVersionsFilter
) {
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
) {
versions(limit: $versionsLimit, cursor: $versionsCursor, filter: $versionsFilter) {
items {
id
referencedObject
@@ -137,11 +127,9 @@ class ModelResource(ResourceBase):
"modelId": model_id,
"versionsLimit": versions_limit,
"versionsCursor": versions_cursor,
"versionsFilter": (
versions_filter.model_dump(warnings="error", by_alias=True)
if versions_filter
else None
),
"versionsFilter": versions_filter.model_dump(warnings="error")
if versions_filter
else None,
}
return self.make_request_and_parse_response(
@@ -158,18 +146,9 @@ class ModelResource(ResourceBase):
) -> ResourceCollection[Model]:
QUERY = gql(
"""
query ProjectGetWithModels(
$projectId: String!,
$modelsLimit: Int!,
$modelsCursor: String,
$modelsFilter: ProjectModelsFilter
) {
query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) {
data:project(id: $projectId) {
data:models(
limit: $modelsLimit,
cursor: $modelsCursor,
filter: $modelsFilter
) {
data:models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) {
items {
id
name
@@ -200,11 +179,9 @@ class ModelResource(ResourceBase):
"projectId": project_id,
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": (
models_filter.model_dump(warnings="error", by_alias=True)
if models_filter
else None
),
"modelsFilter": models_filter.model_dump(warnings="error")
if models_filter
else None,
}
return self.make_request_and_parse_response(
@@ -240,7 +217,7 @@ class ModelResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -258,7 +235,7 @@ class ModelResource(ResourceBase):
"""
)
variables = {"input": input.model_dump(warnings="error", by_alias=True)}
variables = {"input": input.model_dump(warnings="error")}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
@@ -293,7 +270,7 @@ class ModelResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -1,13 +1,21 @@
from typing import Optional
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.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
from specklepy.logging.exceptions import SpeckleException
NAME = "other_user"
@@ -66,9 +74,7 @@ class OtherUserResource(ResourceBase):
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
"""Searches for a user on the server, by name or email. The search query must be at least
3 characters long
Arguments:
@@ -83,20 +89,8 @@ class OtherUserResource(ResourceBase):
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
) {
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
@@ -122,3 +116,124 @@ class OtherUserResource(ResourceBase):
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]:
"""Searches for user by name or email. The search query must be at least
3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[LimitedUser] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
)
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
role
}
}
}
"""
)
params = {"search_query": search_query, "limit": limit}
return self.make_request(
query=query, params=params, return_type=["userSearch", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of
any tz as they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$user_id: String!,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
otherUser(id: $user_id) {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"user_id": user_id,
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["otherUser", "activity"],
schema=ActivityCollection,
)
@@ -37,10 +37,7 @@ class ProjectInviteResource(ResourceBase):
) -> ProjectWithTeam:
QUERY = gql(
"""
mutation ProjectInviteCreate(
$projectId: ID!,
$input: ProjectInviteCreateInput!
) {
mutation ProjectInviteCreate($projectId: ID!, $input: ProjectInviteCreateInput!) {
data:projectMutations {
data:invites {
data:create(projectId: $projectId, input: $input) {
@@ -103,7 +100,7 @@ class ProjectInviteResource(ResourceBase):
variables = {
"projectId": project_id,
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -124,7 +121,7 @@ class ProjectInviteResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -7,10 +7,8 @@ from specklepy.core.api.inputs.project_inputs import (
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
WorkspaceProjectCreateInput,
)
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
from specklepy.core.api.models.current import ProjectPermissionChecks
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
@@ -57,36 +55,6 @@ class ProjectResource(ResourceBase):
DataResponse[Project], QUERY, variables
).data
def get_permissions(self, project_id: str) -> ProjectPermissionChecks:
QUERY = gql(
"""
query Project($projectId: String!) {
data:project(id: $projectId) {
data:permissions {
canCreateModel {
authorized
code
message
}
canDelete {
authorized
code
message
}
}
}
}
"""
)
variables = {
"projectId": project_id,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[ProjectPermissionChecks]], QUERY, variables
).data.data
def get_with_models(
self,
project_id: str,
@@ -97,12 +65,7 @@ class ProjectResource(ResourceBase):
) -> ProjectWithModels:
QUERY = gql(
"""
query ProjectGetWithModels(
$projectId: String!,
$modelsLimit: Int!,
$modelsCursor: String,
$modelsFilter: ProjectModelsFilter
) {
query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) {
data:project(id: $projectId) {
id
name
@@ -114,11 +77,7 @@ class ProjectResource(ResourceBase):
updatedAt
sourceApps
workspaceId
models(
limit: $modelsLimit,
cursor: $modelsCursor,
filter: $modelsFilter
) {
models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) {
items {
id
name
@@ -149,11 +108,9 @@ class ProjectResource(ResourceBase):
"projectId": project_id,
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": (
models_filter.model_dump(warnings="error", by_alias=True)
if models_filter
else None
),
"modelsFilter": models_filter.model_dump(warnings="error")
if models_filter
else None,
}
return self.make_request_and_parse_response(
@@ -230,12 +187,6 @@ class ProjectResource(ResourceBase):
).data
def create(self, input: ProjectCreateInput) -> Project:
"""
Creates a non-workspace project (aka Personal Project)
see client.active_user.can_create_personal_projects to see if the user has
permission
"""
QUERY = gql(
"""
mutation ProjectCreate($input: ProjectCreateInput) {
@@ -258,52 +209,13 @@ class ProjectResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Project]], QUERY, variables
).data.data
def create_in_workspace(self, input: WorkspaceProjectCreateInput) -> Project:
"""
Creates a workspace project
This feature is only supported by Workspace Enabled Servers
(e.g. app.speckle.systems)
see `workspace.permissions.can_create_project` to see if the user has permission
"""
QUERY = gql(
"""
mutation WorkspaceProjectCreate($input: WorkspaceProjectCreateInput!) {
data:workspaceMutations {
data:projects {
data:create(input: $input) {
id
name
description
visibility
allowPublicComments
role
createdAt
updatedAt
sourceApps
workspaceId
}
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[Project]]], QUERY, variables
).data.data.data
def update(self, input: ProjectUpdateInput) -> Project:
QUERY = gql(
"""
@@ -327,7 +239,7 @@ class ProjectResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -416,7 +328,7 @@ class ProjectResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -1,6 +1,7 @@
import re
from typing import Any, Dict, List, Tuple
import requests
from gql import gql
from specklepy.core.api.models import ServerInfo
@@ -37,6 +38,11 @@ class ServerResource(ResourceBase):
adminContact
canonicalUrl
version
roles {
name
description
resourceTarget
}
scopes {
name
description
@@ -46,9 +52,6 @@ class ServerResource(ResourceBase):
name
icon
}
workspaces {
workspacesEnabled
}
}
}
"""
@@ -57,6 +60,16 @@ class ServerResource(ResourceBase):
server_info = self.make_request(
query=query, return_type="serverInfo", schema=ServerInfo
)
if isinstance(server_info, ServerInfo) and isinstance(
server_info.canonicalUrl, str
):
r = requests.get(
server_info.canonicalUrl, headers={"User-Agent": "specklepy SDK"}
)
if "x-speckle-frontend-2" in r.headers:
server_info.frontend2 = True
else:
server_info.frontend2 = False
return server_info
@@ -67,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 {
@@ -76,13 +76,7 @@ class VersionResource(ResourceBase):
) -> ResourceCollection[Version]:
QUERY = gql(
"""
query VersionGetVersions(
$projectId: String!,
$modelId: String!,
$limit: Int!,
$cursor: String,
$filter: ModelVersionsFilter
) {
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) {
@@ -117,9 +111,7 @@ class VersionResource(ResourceBase):
"modelId": model_id,
"limit": limit,
"cursor": cursor,
"filter": (
filter.model_dump(warnings="error", by_alias=True) if filter else None
),
"filter": filter.model_dump(warnings="error") if filter else None,
}
return self.make_request_and_parse_response(
@@ -128,39 +120,26 @@ class VersionResource(ResourceBase):
variables,
).data.data.data
def create(self, input: CreateVersionInput) -> Version:
def create(self, input: CreateVersionInput) -> str:
QUERY = gql(
"""
mutation Create($input: CreateVersionInput!) {
data:versionMutations {
data:create(input: $input) {
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
data:id
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Version]], QUERY, variables
).data.data
DataResponse[DataResponse[DataResponse[str]]], QUERY, variables
).data.data.data
def update(self, input: UpdateVersionInput) -> Version:
QUERY = gql(
@@ -189,7 +168,7 @@ class VersionResource(ResourceBase):
"""
)
variables = {"input": input.model_dump(warnings="error", by_alias=True)}
variables = {"input": input.model_dump(warnings="error")}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Version]], QUERY, variables
@@ -209,7 +188,7 @@ class VersionResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -228,7 +207,7 @@ class VersionResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -247,7 +226,7 @@ class VersionResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error", by_alias=True),
"input": input.model_dump(warnings="error"),
}
return self.make_request_and_parse_response(
@@ -1,106 +0,0 @@
from typing import Optional
from gql import gql
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
from specklepy.core.api.models.current import Project, ResourceCollection, Workspace
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
NAME = "workspace"
class WorkspaceResource(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, workspace_id: str) -> Workspace:
QUERY = gql(
"""
query WorkspaceGet($workspaceId: String!) {
data:workspace(id: $workspaceId) {
id
name
role
slug
logo
createdAt
updatedAt
readOnly
description
creationState
{
completed
}
permissions {
canCreateProject {
authorized
code
message
}
}
}
}
"""
)
variables = {
"workspaceId": workspace_id,
}
return self.make_request_and_parse_response(
DataResponse[Workspace], QUERY, variables
).data
def get_projects(
self,
workspace_id: str,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[WorksaceProjectsFilter] = None,
) -> ResourceCollection[Project]:
QUERY = gql(
"""
query Workspace($workspaceId: String!, $limit: Int!, $cursor: String, $filter: WorkspaceProjectsFilter) {
data:workspace(id: $workspaceId) {
data:projects(limit: $limit, cursor: $cursor, filter: $filter) {
cursor
items {
allowPublicComments
createdAt
description
id
name
role
sourceApps
updatedAt
visibility
workspaceId
}
totalCount
}
}
}
""" # noqa: E501
)
variables = {
"workspaceId": workspace_id,
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error", by_alias=True)
if filter
else None,
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[ResourceCollection[Project]]], QUERY, variables
).data.data
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,235 @@
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.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "branch"
class Resource(ResourceBase):
"""
API Access class for branches
Branch resource is deprecated, please use model resource instead
"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
)
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:
"""Create a new branch on this stream
Arguments:
name {str} -- the name of the new branch
description {str} -- a short description of the branch
Returns:
id {str} -- the newly created branch's id
"""
query = gql(
"""
mutation BranchCreate($branch: BranchCreateInput!) {
branchCreate(branch: $branch)
}
"""
)
if len(name) < 3:
return SpeckleException(message="Branch Name must be at least 3 characters")
params = {
"branch": {
"streamId": stream_id,
"name": name,
"description": description,
}
}
return self.make_request(
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
Arguments:
stream_id {str} -- the id of the stream to get the branch from
name {str} -- the name of the branch to get
commits_limit {int} -- maximum number of commits to get
Returns:
Branch -- the fetched branch with its latest commits
"""
query = gql(
"""
query BranchGet($stream_id: String!, $name: String!, $commits_limit: Int!) {
stream(id: $stream_id) {
branch(name: $name) {
id,
name,
description,
commits (limit: $commits_limit) {
totalCount,
cursor,
items {
id,
referencedObject,
sourceApplication,
totalChildrenCount,
message,
authorName,
authorId,
branchName,
parents,
createdAt
}
}
}
}
}
"""
)
params = {"stream_id": stream_id, "name": name, "commits_limit": commits_limit}
return self.make_request(
query=query, params=params, return_type=["stream", "branch"]
)
@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
Arguments:
stream_id {str} -- the id of the stream to get the branches from
branches_limit {int} -- maximum number of branches to get
commits_limit {int} -- maximum number of commits to get
Returns:
List[Branch] -- the branches on the stream
"""
query = gql(
"""
query BranchesGet(
$stream_id: String!,
$branches_limit: Int!,
$commits_limit: Int!
) {
stream(id: $stream_id) {
branches(limit: $branches_limit) {
items {
id
name
description
commits(limit: $commits_limit) {
totalCount
items{
id
message
referencedObject
sourceApplication
parents
authorId
authorName
branchName
createdAt
}
}
}
}
}
}
"""
)
params = {
"stream_id": stream_id,
"branches_limit": branches_limit,
"commits_limit": commits_limit,
}
return self.make_request(
query=query, params=params, return_type=["stream", "branches", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
stream_id: str,
branch_id: str,
name: Optional[str] = None,
description: Optional[str] = None,
):
"""Update a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to update
branch_id {str} -- the id of the branch to update
name {str} -- optional: the updated branch name
description {str} -- optional: the updated branch description
Returns:
bool -- True if update is successful
"""
query = gql(
"""
mutation BranchUpdate($branch: BranchUpdateInput!) {
branchUpdate(branch: $branch)
}
"""
)
params = {
"branch": {
"streamId": stream_id,
"id": branch_id,
}
}
if name:
params["branch"]["name"] = name
if description:
params["branch"]["description"] = description
return self.make_request(
query=query, params=params, return_type="branchUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to delete
branch_id {str} -- the branch to delete
Returns:
bool -- True if deletion is successful
"""
query = gql(
"""
mutation BranchDelete($branch: BranchDeleteInput!) {
branchDelete(branch: $branch)
}
"""
)
params = {"branch": {"streamId": stream_id, "id": branch_id}}
return self.make_request(
query=query, params=params, return_type="branchDelete", parse_response=False
)
@@ -0,0 +1,252 @@
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.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "commit"
class Resource(ResourceBase):
"""
API Access class for commits
Commit resource is deprecated, please use version resource instead
"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
)
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
Arguments:
stream_id {str} -- the stream where we can find the commit
commit_id {str} -- the id of the commit you want to get
Returns:
Commit -- the retrieved commit object
"""
query = gql(
"""
query Commit($stream_id: String!, $commit_id: String!) {
stream(id: $stream_id) {
commit(id: $commit_id) {
id
message
referencedObject
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
parents
}
}
}
"""
)
params = {"stream_id": stream_id, "commit_id": commit_id}
return self.make_request(
query=query, params=params, return_type=["stream", "commit"]
)
@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
Arguments:
stream_id {str} -- the stream where the commits are
limit {int} -- the maximum number of commits to fetch (default = 10)
Returns:
List[Commit] -- a list of the most recent commit objects
"""
query = gql(
"""
query Commits($stream_id: String!, $limit: Int!) {
stream(id: $stream_id) {
commits(limit: $limit) {
items {
id
message
referencedObject
authorName
authorId
authorName
authorAvatar
branchName
createdAt
sourceApplication
totalChildrenCount
parents
}
}
}
}
"""
)
params = {"stream_id": stream_id, "limit": limit}
return self.make_request(
query=query, params=params, return_type=["stream", "commits", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
stream_id: str,
object_id: str,
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
"""
Creates a commit on a branch
Arguments:
stream_id {str} -- the stream you want to commit to
object_id {str} -- the hash of your commit object
branch_name {str}
-- the name of the branch to commit to (defaults to "main")
message {str}
-- optional: a message to give more information about the commit
source_application{str}
-- optional: the application from which the commit was created
(defaults to "python")
parents {List[str]} -- optional: the id of the parent commits
Returns:
str -- the id of the created commit
"""
query = gql(
"""
mutation CommitCreate ($commit: CommitCreateInput!)
{ commitCreate(commit: $commit)}
"""
)
params = {
"commit": {
"streamId": stream_id,
"branchName": branch_name,
"objectId": object_id,
"message": message,
"sourceApplication": source_application,
}
}
if parents:
params["commit"]["parents"] = parents
return self.make_request(
query=query, params=params, return_type="commitCreate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
Update a commit
Arguments:
stream_id {str}
-- the id of the stream that contains the commit you'd like to update
commit_id {str} -- the id of the commit you'd like to update
message {str} -- the updated commit message
Returns:
bool -- True if the operation succeeded
"""
query = gql(
"""
mutation CommitUpdate($commit: CommitUpdateInput!)
{ commitUpdate(commit: $commit)}
"""
)
params = {
"commit": {"streamId": stream_id, "id": commit_id, "message": message}
}
return self.make_request(
query=query, params=params, return_type="commitUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
Delete a commit
Arguments:
stream_id {str}
-- the id of the stream that contains the commit you'd like to delete
commit_id {str} -- the id of the commit you'd like to delete
Returns:
bool -- True if the operation succeeded
"""
query = gql(
"""
mutation CommitDelete($commit: CommitDeleteInput!)
{ commitDelete(commit: $commit)}
"""
)
params = {"commit": {"streamId": stream_id, "id": commit_id}}
return self.make_request(
query=query, params=params, return_type="commitDelete", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def received(
self,
stream_id: str,
commit_id: str,
source_application: str = "python",
message: Optional[str] = None,
) -> bool:
"""
Mark a commit object a received by the source application.
"""
query = gql(
"""
mutation CommitReceive($receivedInput:CommitReceivedInput!){
commitReceive(input:$receivedInput)
}
"""
)
params = {
"receivedInput": {
"sourceApplication": source_application,
"streamId": stream_id,
"commitId": commit_id,
"message": "message",
}
}
try:
return self.make_request(
query=query,
params=params,
return_type="commitReceive",
parse_response=False,
)
except Exception as ex:
print(ex.with_traceback)
return False
@@ -0,0 +1,92 @@
from typing import Dict, List
from gql import gql
from specklepy.core.api.resource import ResourceBase
from specklepy.objects.base import Base
NAME = "object"
class Resource(ResourceBase):
"""API Access class for objects"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
)
self.schema = Base
def get(self, stream_id: str, object_id: str) -> Base:
"""
Get a stream object
Arguments:
stream_id {str} -- the id of the stream for the object
object_id {str} -- the hash of the object you want to get
Returns:
Base -- the returned Base object
"""
query = gql(
"""
query Object($stream_id: String!, $object_id: String!) {
stream(id: $stream_id) {
id
name
object(id: $object_id) {
id
speckleType
applicationId
createdAt
totalChildrenCount
data
}
}
}
"""
)
params = {"stream_id": stream_id, "object_id": object_id}
return self.make_request(
query=query,
params=params,
return_type=["stream", "object", "data"],
)
def create(self, stream_id: str, objects: List[Dict]) -> str:
"""
Not advised - generally, you want to use `operations.send()`.
Create a new object on a stream.
To send a base object, you can prepare it by running it through the
`BaseObjectSerializer.traverse_base()` function to get a valid (serialisable)
object to send.
NOTE: this does not create a commit - you can create one with
`SpeckleClient.commit.create`.
Dynamic fields will be located in the 'data' dict of the received `Base` object
Arguments:
stream_id {str} -- the id of the stream you want to send the object to
objects {List[Dict]}
-- a list of base dictionary objects (NOTE: must be json serialisable)
Returns:
str -- the id of the object
"""
query = gql(
"""
mutation ObjectCreate($object_input: ObjectCreateInput!) {
objectCreate(objectInput: $object_input)
}
"""
)
params = {"object_input": {"streamId": stream_id, "objects": objects}}
return self.make_request(
query=query, params=params, return_type="objectCreate", parse_response=False
)
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,11 @@
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"""
@@ -0,0 +1,785 @@
from datetime import datetime, timezone
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Stream,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
NAME = "stream"
class Resource(ResourceBase):
"""
API Access class for streams
Stream resource is deprecated, please use project resource instead
"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = Stream
@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
Arguments:
id {str} -- the stream id
branch_limit {int} -- the maximum number of branches to return
commit_limit {int} -- the maximum number of commits to return
Returns:
Stream -- the retrieved stream
"""
query = gql(
"""
query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) {
stream(id: $id) {
id
name
role
description
isPublic
createdAt
updatedAt
commentCount
favoritesCount
collaborators {
id
name
role
avatar
}
branches(limit: $branch_limit) {
totalCount
cursor
items {
id
name
description
commits(limit: $commit_limit) {
totalCount
cursor
items {
id
message
authorId
createdAt
authorName
referencedObject
sourceApplication
}
}
}
}
}
}
"""
)
params = {"id": id, "branch_limit": branch_limit, "commit_limit": commit_limit}
return self.make_request(query=query, params=params, return_type="stream")
@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
Arguments:
stream_limit {int} -- The maximum number of streams to return
Returns:
List[Stream] -- A list of Stream objects
"""
query = gql(
"""
query User($stream_limit: Int!) {
user {
id
bio
name
email
avatar
company
verified
profiles
role
streams(limit: $stream_limit) {
totalCount
cursor
items {
id
name
role
isPublic
createdAt
updatedAt
description
commentCount
favoritesCount
collaborators {
id
name
role
}
}
}
}
}
"""
)
params = {"stream_limit": stream_limit}
return self.make_request(
query=query, params=params, return_type=["user", "streams", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
name: str = "Anonymous Python Stream",
description: str = "No description provided",
is_public: bool = True,
) -> str:
"""Create a new stream
Arguments:
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
id {str} -- the id of the newly created stream
"""
query = gql(
"""
mutation StreamCreate($stream: StreamCreateInput!) {
streamCreate(stream: $stream)
}
"""
)
if len(name) < 3 and len(name) != 0:
return SpeckleException(message="Stream Name must be at least 3 characters")
params = {
"stream": {"name": name, "description": description, "isPublic": is_public}
}
return self.make_request(
query=query, params=params, return_type="streamCreate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
id: str,
name: Optional[str] = None,
description: Optional[str] = None,
is_public: Optional[bool] = None,
) -> bool:
"""Update an existing stream
Arguments:
id {str} -- the id of the stream to be updated
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
bool -- whether the stream update was successful
"""
query = gql(
"""
mutation StreamUpdate($stream: StreamUpdateInput!) {
streamUpdate(stream: $stream)
}
"""
)
params = {
"id": id,
"name": name,
"description": description,
"isPublic": is_public,
}
# remove None values so graphql doesn't cry
params = {"stream": {k: v for k, v in params.items() if v is not None}}
return self.make_request(
query=query, params=params, return_type="streamUpdate", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, id: str) -> bool:
"""Delete a stream given its id
Arguments:
id {str} -- the id of the stream to delete
Returns:
bool -- whether the deletion was successful
"""
query = gql(
"""
mutation StreamDelete($id: String!) {
streamDelete(id: $id)
}
"""
)
params = {"id": id}
return self.make_request(
query=query, params=params, return_type="streamDelete", parse_response=False
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def search(
self,
search_query: str,
limit: int = 25,
branch_limit: int = 10,
commit_limit: int = 10,
):
"""Search for streams by name, description, or id
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
branch_limit {int} -- the maximum number of branches to return
commit_limit {int} -- the maximum number of commits to return
Returns:
List[Stream] -- a list of Streams that match the search query
"""
query = gql(
"""
query StreamSearch(
$search_query: String!,
$limit: Int!,
$branch_limit:Int!,
$commit_limit:Int!
) {
streams(query: $search_query, limit: $limit) {
items {
id
name
role
description
isPublic
createdAt
updatedAt
collaborators {
id
name
role
avatar
}
branches(limit: $branch_limit) {
totalCount
cursor
items {
id
name
description
commits(limit: $commit_limit) {
totalCount
cursor
items {
id
referencedObject
message
authorName
authorId
createdAt
}
}
}
}
}
}
}
"""
)
params = {
"search_query": search_query,
"limit": limit,
"branch_limit": branch_limit,
"commit_limit": commit_limit,
}
return self.make_request(
query=query, params=params, return_type=["streams", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream.
Arguments:
stream_id {str} -- the id of the stream to favorite / unfavorite
favorited {bool}
-- whether to favorite (True) or unfavorite (False) the stream
Returns:
Stream -- the stream with its `id`, `name`, and `favoritedDate`
"""
query = gql(
"""
mutation StreamFavorite($stream_id: String!, $favorited: Boolean!) {
streamFavorite(streamId: $stream_id, favorited: $favorited) {
id
name
favoritedDate
favoritesCount
}
}
"""
)
params = {
"stream_id": stream_id,
"favorited": favorited,
}
return self.make_request(
query=query, params=params, return_type=["streamFavorite"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(
self, stream_id: str
) -> List[PendingStreamCollaborator]:
"""Get all of the pending invites on a stream.
You must be a `stream:owner` to query this.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the stream id from which to get the pending invites
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the specified stream
"""
self._check_invites_supported()
query = gql(
"""
query StreamInvites($streamId: String!) {
stream(id: $streamId){
pendingCollaborators {
id
token
inviteId
streamId
streamName
projectName
projectId
title
role
invitedBy{
id
name
bio
company
avatar
verified
role
}
user {
id
name
bio
company
avatar
verified
role
}
}
}
}
"""
)
params = {"streamId": stream_id}
return self.make_request(
query=query,
params=params,
return_type=["stream", "pendingCollaborators"],
schema=PendingStreamCollaborator,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite(
self,
stream_id: str,
email: Optional[str] = None,
user_id: Optional[str] = None,
role: str = "stream:contributor", # should default be reviewer?
message: Optional[str] = None,
):
"""Invite someone to a stream using either their email or user id
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
email {str} -- the email of the user to invite (use this OR `user_id`)
user_id {str} -- the id of the user to invite (use this OR `email`)
role {str}
-- the role to assign to the user (defaults to `stream:contributor`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
self._check_invites_supported()
if email is None and user_id is None:
raise SpeckleException(
"You must provide either an email or a user id to use the"
" `stream.invite` method"
)
query = gql(
"""
mutation StreamInviteCreate($input: StreamInviteCreateInput!) {
streamInviteCreate(input: $input)
}
"""
)
params = {
"email": email,
"userId": user_id,
"streamId": stream_id,
"message": message,
"role": role,
}
params = {"input": {k: v for k, v in params.items() if v is not None}}
return self.make_request(
query=query,
params=params,
return_type="streamInviteCreate",
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_batch(
self,
stream_id: str,
emails: Optional[List[str]] = None,
user_ids: Optional[List[None]] = None,
message: Optional[str] = None,
) -> bool:
"""Invite a batch of users to a specified stream.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
emails {List[str]}
-- the email of the user to invite (use this and/or `user_ids`)
user_id {List[str]}
-- the id of the user to invite (use this and/or `emails`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
self._check_invites_supported()
if emails is None and user_ids is None:
raise SpeckleException(
"You must provide either an email or a user id to use the"
" `stream.invite` method"
)
query = gql(
"""
mutation StreamInviteBatchCreate($input: [StreamInviteCreateInput!]!) {
streamInviteBatchCreate(input: $input)
}
"""
)
email_invites = [
{"streamId": stream_id, "message": message, "email": email}
for email in (emails if emails is not None else [])
if email is not None
]
user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id}
for user_id in (user_ids if user_ids is not None else [])
if user_id is not None
]
params = {"input": [*email_invites, *user_invites]}
return self.make_request(
query=query,
params=params,
return_type="streamInviteBatchCreate",
parse_response=False,
)
@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
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream invite
invite_id {str} -- the id of the invite to use
Returns:
bool -- true if the operation was successful
"""
self._check_invites_supported()
query = gql(
"""
mutation StreamInviteCancel($streamId: String!, $inviteId: String!) {
streamInviteCancel(streamId: $streamId, inviteId: $inviteId)
}
"""
)
params = {"streamId": stream_id, "inviteId": invite_id}
return self.make_request(
query=query,
params=params,
return_type="streamInviteCancel",
parse_response=False,
)
@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
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str}
-- the id of the stream for which the user has a pending invite
token {str} -- the token of the invite to use
accept {bool} -- whether or not to accept the invite (defaults to True)
Returns:
bool -- true if the operation was successful
"""
self._check_invites_supported()
query = gql(
"""
mutation StreamInviteUse(
$accept: Boolean!,
$streamId: String!,
$token: String!
) {
streamInviteUse(accept: $accept, streamId: $streamId, token: $token)
}
"""
)
params = {"streamId": stream_id, "token": token, "accept": accept}
return self.make_request(
query=query,
params=params,
return_type="streamInviteUse",
parse_response=False,
)
@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
Valid for Speckle Server >=2.6.4
Arguments:
stream_id {str} -- the id of the stream to grant permissions to
user_id {str} -- the id of the user to grant permissions for
role {str} -- the role to grant the user
Returns:
bool -- True if the operation was successful
"""
if self.server_version and (
self.server_version != ("dev",) and self.server_version < (2, 6, 4)
):
raise UnsupportedException(
"Server mutation `update_permission` is only supported as of Speckle"
" Server v2.6.4. Please update your Speckle Server to use this method"
" or use the `grant_permission` method instead."
)
query = gql(
"""
mutation StreamUpdatePermission(
$permission_params: StreamUpdatePermissionInput!
) {
streamUpdatePermission(permissionParams: $permission_params)
}
"""
)
params = {
"permission_params": {
"streamId": stream_id,
"userId": user_id,
"role": role,
}
}
return self.make_request(
query=query,
params=params,
return_type="streamUpdatePermission",
parse_response=False,
)
@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
Arguments:
stream_id {str} -- the id of the stream to revoke permissions from
user_id {str} -- the id of the user to revoke permissions from
Returns:
bool -- True if the operation was successful
"""
query = gql(
"""
mutation StreamRevokePermission(
$permission_params: StreamRevokePermissionInput!
) {
streamRevokePermission(permissionParams: $permission_params)
}
"""
)
params = {"permission_params": {"streamId": stream_id, "userId": user_id}}
return self.make_request(
query=query,
params=params,
return_type="streamRevokePermission",
parse_response=False,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
stream_id: str,
action_type: Optional[str] = None,
limit: int = 20,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of any tz
as they will be converted to UTC ISO format strings
stream_id {str} -- the id of the stream to get activity from
action_type {str}
-- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime}
-- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query StreamActivity(
$stream_id: String!,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
stream(id: $stream_id) {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
try:
params = {
"stream_id": stream_id,
"limit": limit,
"action_type": action_type,
"before": (
before.astimezone(timezone.utc).isoformat() if before else before
),
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": (
cursor.astimezone(timezone.utc).isoformat() if cursor else cursor
),
}
except AttributeError as e:
raise SpeckleException(
"Could not get stream activity - `before`, `after`, and `cursor` must"
" be in `datetime` format if provided",
ValueError(),
) from e
return self.make_request(
query=query,
params=params,
return_type=["stream", "activity"],
schema=ActivityCollection,
)
@@ -0,0 +1,144 @@
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.logging.exceptions import SpeckleException
NAME = "subscribe"
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(ResourceBase):
"""API Access class for subscriptions"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
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.
Use this to display an up-to-date list of streams.
Arguments:
callback {Callable[Stream]} -- a function that takes the updated stream
as an argument and executes each time a stream is added
Returns:
Stream -- the update stream
"""
query = gql(
"""
subscription { userStreamAdded }
"""
)
return await self.subscribe(
query=query, callback=callback, return_type="userStreamAdded", schema=Stream
)
@check_wsclient
async def stream_updated(self, id: str, callback: Optional[Callable] = None):
"""
Subscribes to stream updated event.
Use this in clients/components that pertain only to this stream.
Arguments:
id {str} -- the stream id of the stream to subscribe to
callback {Callable[Stream]}
-- a function that takes the updated stream
as an argument and executes each time the stream is updated
Returns:
Stream -- the update stream
"""
query = gql(
"""
subscription Update($id: String!) { streamUpdated(streamId: $id) }
"""
)
params = {"id": id}
return await self.subscribe(
query=query,
params=params,
callback=callback,
return_type="streamUpdated",
schema=Stream,
)
@check_wsclient
async def stream_removed(self, callback: Optional[Callable] = None):
"""Subscribes to stream removed event for your profile.
Use this to display an up-to-date list of streams for your profile.
NOTE: If someone revokes your permissions on a stream,
this subscription will be triggered with an extra value of revokedBy
in the payload.
Arguments:
callback {Callable[Dict]}
-- a function that takes the returned dict as an argument
and executes each time a stream is removed
Returns:
dict -- dict containing 'id' of stream removed and optionally 'revokedBy'
"""
query = gql(
"""
subscription { userStreamRemoved }
"""
)
return await self.subscribe(
query=query,
callback=callback,
return_type="userStreamRemoved",
parse_response=False,
)
@check_wsclient
async def subscribe(
self,
query: DocumentNode,
params: Optional[Dict] = None,
callback: Optional[Callable] = None,
return_type: Optional[Union[str, List]] = None,
schema=None,
parse_response: bool = True,
):
# if self.client.transport.websocket is None:
# TODO: add multiple subs to the same ws connection
async with self.client as session:
async for res in session.subscribe(query, variable_values=params):
res = self._step_into_response(response=res, return_type=return_type)
if parse_response:
res = self._parse_response(response=res, schema=schema)
if callback is not None:
callback(res)
else:
return res
@@ -0,0 +1,325 @@
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,
PendingStreamCollaborator,
User,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "user"
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = (
"The user resource is deprecated, please use the active_user or other_user"
" resources"
)
class Resource(ResourceBase):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=NAME,
server_version=server_version,
)
self.schema = User
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get(self, id: Optional[str] = None) -> User:
"""
Gets the profile of a user.
If no id argument is provided, will return the current authenticated
user's profile (as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
query = gql(
"""
query User($id: String) {
user(id: $id) {
id
email
name
bio
company
avatar
verified
profiles
role
}
}
"""
)
params = {"id": id}
return self.make_request(query=query, params=params, return_type="user")
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[User], SpeckleException]:
"""
Searches for user by name or email.
The search query must be at least 3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[User] -- a list of User objects that match the search query
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters"
)
query = gql(
"""
query UserSearch($search_query: String!, $limit: Int!) {
userSearch(query: $search_query, limit: $limit) {
items {
id
name
bio
company
avatar
verified
}
}
}
"""
)
params = {"search_query": search_query, "limit": limit}
return self.make_request(
query=query, params=params, return_type=["userSearch", "items"]
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns:
bool -- True if your profile was updated successfully
"""
query = gql(
"""
mutation UserUpdate($user: UserUpdateInput!) {
userUpdate(user: $user)
}
"""
)
params = {"name": name, "company": company, "bio": bio, "avatar": avatar}
params = {"user": {k: v for k, v in params.items() if v is not None}}
if not params["user"]:
return SpeckleException(
message=(
"You must provide at least one field to update your user profile"
)
)
return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def activity(
self,
user_id: Optional[str] = None,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated
user's activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as
they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime}
-- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$user_id: String,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
user(id: $user_id) {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"user_id": user_id,
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["user", "activity"],
schema=ActivityCollection,
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
self._check_invites_supported()
query = gql(
"""
query StreamInvites {
streamInvites{
id
token
inviteId
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
return self.make_request(
query=query,
return_type="streamInvites",
schema=PendingStreamCollaborator,
)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
self._check_invites_supported()
query = gql(
"""
query StreamInvite($streamId: String!, $token: String) {
streamInvite(streamId: $streamId, token: $token) {
id
token
streamId
streamName
title
role
invitedBy {
id
name
company
avatar
}
}
}
"""
)
params = {"streamId": stream_id}
if token:
params["token"] = token
return self.make_request(
query=query,
params=params,
return_type="streamInvite",
schema=PendingStreamCollaborator,
)
+8 -19
View File
@@ -18,7 +18,7 @@ class StreamWrapper:
The `StreamWrapper` gives you some handy helpers to deal with urls and
get authenticated clients and transports.
Construct a `StreamWrapper` with a URL of a model, version, or object.
Construct a `StreamWrapper` with a stream, branch, commit, or object URL.
The corresponding ids will be stored
in the wrapper. If you have local accounts on the machine,
you can use the `get_account` and `get_client` methods
@@ -29,8 +29,8 @@ class StreamWrapper:
```py
from specklepy.api.wrapper import StreamWrapper
# provide a url for a model, version, or object
wrapper = StreamWrapper("https://app.speckle.systems/projects/3073b96e86/models/0fe47c9dca@604bea8cc6")
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
@@ -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:
-5
View File
@@ -58,8 +58,3 @@ class UnsupportedException(SpeckleException):
class SpeckleWarning(Warning):
def __init__(self, *args: object) -> None:
super().__init__(*args)
class WorkspacePermissionException(SpeckleException):
def __init__(self, message: str) -> None:
super().__init__(message=message)
+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]
+9 -3
View File
@@ -1,7 +1,13 @@
from .data_objects import Base, DataObject, QgisObject
"""Builtin Speckle object kit."""
from specklepy.objects import encoding, geometry, other, primitive, units
from specklepy.objects.base import Base
__all__ = [
"Base",
"DataObject",
"QgisObject",
"encoding",
"geometry",
"other",
"units",
"primitive",
]
@@ -1,8 +0,0 @@
from .text import AlignmentHorizontal, AlignmentVertical, Text
# re-export them at the geometry package level
__all__ = [
"Text",
"AlignmentHorizontal",
"AlignmentVertical",
]
-54
View File
@@ -1,54 +0,0 @@
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry import Plane, Point
from specklepy.objects.interfaces import IHasUnits
class AlignmentHorizontal(Enum):
Left = 0
Center = 1
Right = 2
class AlignmentVertical(Enum):
Top = 0
Center = 1
Bottom = 2
@dataclass(kw_only=True)
class Text(Base, IHasUnits, speckle_type="Objects.Annotation.Text"):
"""
Text class for representation in the viewer.
Units will be 'Units.None' if the text size is defined in pixels.
"""
value: str # Plain text, without formatting
origin: Point # Relation to the text is defined by AlignmentH and AlignmentV
height: float # Font height in linear units or pixels (if Units.None)
alignmentH: AlignmentHorizontal = field(
default_factory=lambda: AlignmentHorizontal.Left
)
alignmentV: AlignmentVertical = field(default_factory=lambda: AlignmentVertical.Top)
plane: Optional[Plane] = field(
default_factory=lambda: None
) # None if the text object orientation follows camera view
maxWidth: Optional[float] = field(
default_factory=lambda: None
) # Maximum width of the text field. None, if don't split into lines
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"value: {self.value}, "
f"origin: {self.origin}, "
f"height: {self.height}, "
f"alignmentH: {self.alignmentH}, "
f"alignmentV: {self.alignmentV}, "
f"plane: {self.plane}, "
f"maxWidth: {self.maxWidth}, "
f"units: {self.units})"
)
+38 -37
View File
@@ -1,5 +1,4 @@
import contextlib
from dataclasses import dataclass, field
from enum import Enum
from inspect import isclass
from typing import (
@@ -17,9 +16,10 @@ from typing import (
)
from warnings import warn
from pydantic.alias_generators import to_pascal
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.
@@ -147,7 +148,7 @@ class _RegisteringBase:
# convert the module names to PascalCase to match c# namespace naming convention
# also drop specklepy from the beginning
namespace = ".".join(
to_pascal(m)
pascalcase(m)
for m in filter(lambda name: name != "specklepy", cls.__module__.split("."))
)
return f"{namespace}.{cls.__name__}"
@@ -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.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IDataObject, IGisObject, IHasUnits
@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)
@@ -1,9 +1,10 @@
from enum import Enum
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry import Point
from .base import Base
CHUNKABLE_PROPS = {
"vertices": 100,
"faces": 100,
@@ -15,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
@@ -29,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,38 +0,0 @@
from .arc import Arc
from .box import Box
from .circle import Circle
from .control_point import ControlPoint
from .curve import Curve
from .ellipse import Ellipse
from .line import Line
from .mesh import Mesh
from .plane import Plane
from .point import Point
from .point_cloud import PointCloud
from .polycurve import Polycurve
from .polyline import Polyline
from .region import Region
from .spiral import Spiral
from .surface import Surface
from .vector import Vector
# re-export them at the geometry package level
__all__ = [
"Arc",
"Line",
"Mesh",
"Plane",
"Point",
"Polyline",
"Region",
"Vector",
"Box",
"Circle",
"ControlPoint",
"Ellipse",
"PointCloud",
"Polycurve",
"Spiral",
"Surface",
"Curve",
]
-38
View File
@@ -1,38 +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))
)
-40
View File
@@ -1,40 +0,0 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.interfaces import IHasArea, IHasUnits, IHasVolume
from specklepy.objects.primitive import Interval
@dataclass(kw_only=True)
class Box(Base, IHasUnits, IHasArea, IHasVolume, speckle_type="Objects.Geometry.Box"):
"""
a 3-dimensional box oriented on a plane
"""
basePlane: Plane
xSize: Interval
ySize: Interval
zSize: Interval
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"basePlane: {self.basePlane}, "
f"xSize: {self.xSize}, "
f"ySize: {self.ySize}, "
f"zSize: {self.zSize}, "
f"units: {self.units})"
)
@property
def area(self) -> float:
return 2 * (
self.xSize.length * self.ySize.length
+ self.xSize.length * self.zSize.length
+ self.ySize.length * self.zSize.length
)
@property
def volume(self) -> float:
return self.xSize.length * self.ySize.length * self.zSize.length
-35
View File
@@ -1,35 +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, IHasArea, IHasUnits
@dataclass(kw_only=True)
class Circle(Base, IHasUnits, ICurve, IHasArea, speckle_type="Objects.Geometry.Circle"):
"""
a circular curve based on a plane
"""
plane: Plane
center: Point
radius: float
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"plane: {self.plane}, "
f"center: {self.center}, "
f"radius: {self.radius}, "
f"units: {self.units})"
)
@property
def length(self) -> float:
return 2 * math.pi * self.radius
@property
def area(self) -> float:
return math.pi * self.radius**2
@@ -1,22 +0,0 @@
from dataclasses import dataclass
from specklepy.objects.geometry.point import Point
@dataclass(kw_only=True)
class ControlPoint(Point, speckle_type="Objects.Geometry.ControlPoint"):
"""
a single 3-dimensional point with weight
"""
weight: float
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"x: {self.x}, "
f"y: {self.y}, "
f"z: {self.z}, "
f"weight: {self.weight}, "
f"units: {self.units})"
)
-58
View File
@@ -1,58 +0,0 @@
from dataclasses import dataclass
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry.box import Box
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits
@dataclass(kw_only=True)
class Curve(
Base,
ICurve,
IHasArea,
IHasUnits,
speckle_type="Objects.Geometry.Curve",
detachable={"points", "weights", "knots", "displayValue"},
chunkable={"points": 31250, "weights": 31250, "knots": 31250},
):
"""
a NURBS curve
"""
degree: int
periodic: bool
rational: bool
points: List[float]
weights: List[float]
knots: List[float]
closed: bool
displayValue: Polyline
bbox: Optional[Box] = None
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"degree: {self.degree}, "
f"periodic: {self.periodic}, "
f"rational: {self.rational}, "
f"closed: {self.closed}, "
f"units: {self.units})"
)
@property
def length(self) -> float:
return self.__dict__.get("_length", 0.0)
@length.setter
def length(self, value: float) -> None:
self.__dict__["_length"] = value
@property
def area(self) -> float:
return self.__dict__.get("_area", 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__["_area"] = value
-34
View File
@@ -1,34 +0,0 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits
@dataclass(kw_only=True)
class Ellipse(
Base, IHasUnits, ICurve, IHasArea, speckle_type="Objects.Geometry.Ellipse"
):
"""
an ellipse
"""
plane: Plane
first_radius: float
second_radius: float
@property
def length(self) -> float:
return self.__dict__.get("_length", 0.0)
@length.setter
def length(self, value: float) -> None:
self.__dict__["_length"] = value
@property
def area(self) -> float:
return self.__dict__.get("_area", 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__["_area"] = value
-15
View File
@@ -1,15 +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)
-213
View File
@@ -1,213 +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", "vertexNormals"},
chunkable={
"vertices": 31250,
"faces": 62500,
"colors": 62500,
"textureCoordinates": 31250,
"vertexNormals": 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)
vertexNormals: List[float] = field(default_factory=list)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"vertices: {self.vertices_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)} "
f"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})"
)
-39
View File
@@ -1,39 +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__}("
f"x: {self.x}, "
f"y: {self.y}, "
f"z: {self.z}, "
f"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,24 +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 IHasUnits
@dataclass(kw_only=True)
class PointCloud(Base, IHasUnits, speckle_type="Objects.Geometry.PointCloud"):
"""
a collection of 3-dimensional points
"""
points: List[Point]
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"points: {len(self.points)}, "
f"units: {self.units})"
)
# sizes and colors could be added in the future

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