Compare commits

...

54 Commits

Author SHA1 Message Date
Jedd Morgan 7ab787bfb1 fic(ci): Change trigger to use branhc (#408)
Publish Python Package / continuous-integration (3.10) (push) Has been cancelled
Publish Python Package / continuous-integration (3.11) (push) Has been cancelled
Publish Python Package / continuous-integration (3.12) (push) Has been cancelled
Publish Python Package / continuous-integration (3.13) (push) Has been cancelled
Publish Python Package / Build and Publish Python Package (push) Has been cancelled
* Invert check

* empty
2025-04-23 17:03:42 +01:00
Jedd Morgan bbbf373b50 replaced env with correct boolean check (#407) 2025-04-23 16:51:09 +01:00
Dogukan Karatas f34e4a2874 updates publish.yml (#406) 2025-04-23 17:28:02 +02:00
Dogukan Karatas 45ebc375ad feat(specklepy): update github actions (#405)
* updates publish.yml

* added the secrets

* update publish.yml

* updates workflow

* updates workflows

* Update github-action.yml

* updates github action

* updates docker-compose and deletes .devcontainer

* disables pytests

* changes the localhost

* rollback - comment out the tests

* updates the ci pipeline

---------

Co-authored-by: KatKatKateryna <89912278+KatKatKateryna@users.noreply.github.com>
2025-04-23 17:11:31 +02:00
Dogukan Karatas 4c41fa79fc feat(specklepy): publish to pypi (#396)
* updates publish.yml

* added the secrets

* update publish.yml

* updates workflow

* updates workflows

* Update github-action.yml

* updates github action

* updates docker-compose and deletes .devcontainer

* disables pytests

* changes the localhost

* rollback - comment out the tests

---------

Co-authored-by: KatKatKateryna <89912278+KatKatKateryna@users.noreply.github.com>
2025-04-23 16:51:39 +02:00
Jedd Morgan 0aa14ca077 Publish to testpypi every push (#403) 2025-04-22 14:31:18 +01:00
Jedd Morgan 6bfdf8850c Update publish.yml (#402) 2025-04-22 14:25:41 +01:00
KatKatKateryna 22ecd2c2b3 dont ignore props (#401) 2025-04-22 13:43:58 +01:00
Dogukan Karatas f7f9f73e7b feat(specklepy): curve object class (#400)
* adds curve class
2025-04-11 14:09:39 +02:00
Gergő Jedlicska a7bada391b Merge pull request #398 from specklesystems/gergo/nostringcase
gergo/nostringcase
2025-04-01 11:53:03 +02:00
Gergő Jedlicska 81ff5d82cb Merge pull request #399 from specklesystems/Skip-Circle-Ci
Update config.yml
2025-04-01 11:52:28 +02:00
Jedd Morgan d25edbb3d7 Update config.yml 2025-04-01 10:28:34 +01:00
Gergő Jedlicska 7dedff68f4 Merge branch 'v3-dev' of github.com:specklesystems/specklepy into gergo/nostringcase 2025-03-27 15:50:28 +01:00
Gergő Jedlicska d6e31a9752 chore: fix compose file 2025-03-27 15:19:25 +01:00
Gergő Jedlicska 09c61424d7 tests: update some tests with new server standards 2025-03-27 13:56:19 +01:00
Gergő Jedlicska e9bdf0ceb8 chore: update poetry lock 2025-03-24 20:22:03 +01:00
Gergő Jedlicska 7e6174ebc1 chore: remove stringcase as a dependency 2025-03-24 19:47:07 +01:00
Gergő Jedlicska b8ae3ca8c8 Merge pull request #395 from specklesystems/dogukan/override-limited-user-repr
fix (specklepy): removes avatar in version string representation
2025-03-17 18:31:58 +01:00
Dogukan Karatas d690c45b35 overrides repr 2025-03-17 15:37:13 +01:00
KatKatKateryna 5d3a824986 add region class and tests (#393)
* add region class and tests

* syntax

* export class

* typos
2025-03-17 19:32:57 +08:00
Dogukan Karatas 6f56ecb0c0 fix syntax (#392) 2025-03-11 11:40:25 +01:00
Gergő Jedlicska ef5a570dd4 fix main publish url 2025-02-26 12:17:10 +01:00
KatKatKateryna 424d7d9caf fixed speckle_types for proxies (#388) 2025-02-26 07:20:01 +08:00
Gergő Jedlicska 6aa643837a Merge pull request #387 from specklesystems/jrm/fix-docker-compose
fic(ci): docker compose file missing frontend origin env var
2025-02-19 18:35:34 +01:00
Jedd Morgan 32cbb33e10 Add Frontend origin header 2025-02-19 17:04:30 +00:00
Jedd Morgan 51ae6f5978 Fixed __rep__ on mesh (#386) 2025-02-18 17:03:34 +01:00
Dogukan Karatas b64dde152a adds rendermaterial and rendermaterialproxy (#385) 2025-02-18 17:03:08 +01:00
Jedd Morgan d1b6755997 Removes all FE1 client functions (#380)
* Removes all FE1 client functions

* Removed usages of deprecated client functions

* removed trailing deprecated client function

* ruff

* Fixed last failing test
2025-02-18 15:32:14 +00:00
Gergő Jedlicska da6e2d92e0 Merge pull request #384 from specklesystems/jedd/cxpla-167-update-python-automate-sdk-to-use-fe2-api
Update Automate to use FE2 API
2025-02-18 15:55:28 +01:00
Jedd Morgan 37e9c2372f last tweaks 2025-02-18 13:42:42 +00:00
Jedd Morgan a620a358d3 Fixes 2025-02-18 13:03:19 +00:00
Jedd Morgan fd46fbd961 Updated functions 2025-02-18 11:47:41 +00:00
Jedd Morgan 732f28e653 Added alias config for graphql model 2025-02-13 16:10:11 +00:00
Jedd Morgan 7671998541 Updated version create to return a full Version 2025-02-13 12:40:45 +00:00
Jedd Morgan cab9674803 Updated Automate SDK to use new GraphQL functions 2025-02-13 12:33:59 +00:00
Gergő Jedlicska 6c33c61a6d Merge pull request #382 from specklesystems/gergo/fixServerTransportHeader
fix: server transport always accept text/plain
2025-02-12 13:03:00 +01:00
Gergő Jedlicska 71afb1275f fix: server transport always accept text/plain 2025-02-12 12:35:11 +01:00
Dogukan Karatas 1b53410a86 Merge pull request #379 from specklesystems/dogukan/additional_geometry_classes
feat(specklepy): additional geometry classes
2025-02-10 15:54:04 +01:00
Dogukan Karatas 1ba6983573 ignore formatting on automate context 2025-02-10 15:07:29 +01:00
Dogukan Karatas d5a36fa5e3 rebased with v3-dev 2025-02-10 14:42:48 +01:00
Dogukan Karatas b6e47fb820 psuedo-commit 2025-02-10 10:48:59 +01:00
Gergő Jedlicska 06e21154c4 chore: partially fix linting 2025-02-08 15:42:54 +01:00
Gergő Jedlicska adc0c40ab7 only run publish once tests finished 2025-02-08 15:37:26 +01:00
Gergő Jedlicska a44bb92ec4 run tests to protect v3-dev 2025-02-08 15:26:20 +01:00
Gergő Jedlicska bd98244869 use test environment 2025-02-08 15:18:20 +01:00
Gergő Jedlicska 2acfa48b98 publish to test pypi 2025-02-08 15:13:46 +01:00
Gergő Jedlicska a0283b6048 change to hatchling for build backend 2025-02-08 14:21:11 +01:00
Gergő Jedlicska 0e771a68b8 trying dynamic versioning 2025-02-08 13:01:46 +01:00
Gergő Jedlicska 838f9d4969 fix: add empty license files tag 2025-02-07 09:47:09 +01:00
Dogukan Karatas f98c804094 removes abstractmethod implementations 2025-02-05 15:31:25 +01:00
Dogukan Karatas 0382c246b8 adds type hint to point cloud 2025-02-03 12:52:51 +01:00
Dogukan Karatas 0b38fb5a2a updates control point 2025-02-03 12:48:46 +01:00
Dogukan Karatas ff686b4361 formatted 2025-01-29 14:54:03 +01:00
Dogukan Karatas 7857451ec9 adds missing geometries 2025-01-28 15:30:19 +01:00
122 changed files with 3318 additions and 5184 deletions
+2
View File
@@ -11,5 +11,7 @@ jobs:
# Orchestrate our job run sequence
workflows:
build_and_test:
when:
false
jobs:
- build
-27
View File
@@ -1,27 +0,0 @@
# 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
@@ -1,55 +0,0 @@
// 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
@@ -1,44 +0,0 @@
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:
@@ -1,14 +1,12 @@
name: "Specklepy test and build"
on:
# pull_request:
# branches:
# - 'v3-dev'
push:
pull_request:
branches:
- "gergo/uvSetup"
- "v3-dev"
jobs:
ci:
name: continuous-integration
build-and-test:
name: build-and-test
runs-on: ubuntu-latest
strategy:
matrix:
@@ -39,8 +37,8 @@ jobs:
- name: Run pre-commit
run: uv run pre-commit run --all-files
# - name: Run Speckle Server
# run: docker compose up -d
- 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
+95 -33
View File
@@ -1,36 +1,98 @@
# Publish a release to PyPI.
# name: 'Publish to PyPI'
name: "Publish Python Package"
on:
push:
branches:
- "v3-dev"
tags:
- "3.*.*"
# on:
# push:
# branches:
# - 'gergo/uvSetup'
jobs:
build-and-test:
name: continuous-integration
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
# jobs:
# pypi-publish:
# name: Upload to PyPI
# runs-on: ubuntu-latest
# environment:
# name: release
# permissions:
# # For PyPI's trusted publishing.
# id-token: write
# steps:
# - name: 'Install uv'
# uses: astral-sh/setup-uv@v5
# - uses: actions/checkout@v4
# with:
# # This is necessary so that we have the tags.
# fetch-depth: 0
# - uses: mtkennerly/dunamai-action@v1
# with:
# env-var: MY_VERSION
# args: --style semver
# - run: echo $MY_VERSION
# - name: 'Build artifacts'
# run: uv build
# - name: Publish to PyPi
# run: uv publish --publish-url https://test.pypi.org/simple/
steps:
- uses: actions/checkout@v4
# - name: Test package install
# run: uv run --with specklepy --no-project -- python -c "import specklepy"
- 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,6 +2,8 @@
.envrc
reports/
.volumes/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+2 -5
View File
@@ -6,7 +6,7 @@ services:
# Speckle Server dependencies
#######
postgres:
image: "postgres:16-alpine"
image: "postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf"
restart: always
environment:
POSTGRES_DB: speckle
@@ -49,10 +49,6 @@ services:
retries: 30
start_period: 10s
####
# Speckle Server
#######
speckle-server:
image: speckle/speckle-server:latest
restart: always
@@ -79,6 +75,7 @@ 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"
+51 -35
View File
@@ -1,5 +1,6 @@
[project]
dynamic = ["version"]
# version = "3.0.0a1"
name = "specklepy"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
@@ -7,30 +8,31 @@ authors = [{ name = "Speckle Systems", email = "devops@speckle.systems" }]
license = { text = "Apache-2.0" }
requires-python = ">=3.10.0, <4.0"
dependencies = [
"appdirs>=1.4.4",
"attrs>=24.3.0",
"deprecated>=1.2.15",
"gql[requests,websockets]>=3.5.0",
"httpx>=0.28.1",
"pydantic>=2.10.5",
"pydantic-settings>=2.7.1",
"stringcase>=1.2.0",
"ujson>=5.10.0",
"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",
"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",
"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]
@@ -39,10 +41,14 @@ documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[tool.setuptools_scm]
[tool.hatch.version]
source = "vcs"
[tool.hatch.version.raw-options]
local_scheme = "no-local-version"
[tool.commitizen]
name = "cz_conventional_commits"
@@ -54,17 +60,27 @@ exclude = [".venv", "**/*.yml"]
[tool.ruff.lint]
select = [
# pycodestyle
"E",
# Pyflakes
"F",
# pyupgrade
"UP",
# flake8-bugbear
"B",
# flake8-simplify
"SIM",
# isort
"I",
# 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/"
+53 -47
View File
@@ -1,9 +1,11 @@
# 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, Tuple, Union
from typing import Any, Dict, List, Optional, Union
import httpx
from gql import gql
@@ -18,7 +20,9 @@ from speckle_automate.schema import (
)
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api.models import Branch
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.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.transports.memory import MemoryTransport
@@ -99,22 +103,23 @@ class AutomationContext:
# TODO: this is a quick hack to keep implementation consistency.
# Move to proper receive many versions
version_id = self.automation_run_data.triggers[0].payload.version_id
commit = self.speckle_client.commit.get(
self.automation_run_data.project_id, version_id
)
if not commit or not commit.referencedObject:
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.
{"The commit has no referencedObject." if not commit.referencedObject else ""}
Is your environment configured correctly?
project_id: {self.automation_run_data.project_id}
model_id: {self.automation_run_data.triggers[0].payload.model_id}
version_id: {self.automation_run_data.triggers[0].payload.version_id}
"""
)
) from err
base = operations.receive(
commit.referencedObject, self._server_transport, self._memory_transport
version.referenced_object, self._server_transport, self._memory_transport
)
print(
f"It took {self.elapsed():.2f} seconds to receive",
@@ -122,45 +127,48 @@ 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_name: str, version_message: str = ""
) -> Tuple[str, str]:
self, root_object: Base, model_id: str, version_message: str = ""
) -> Version:
"""Save a base model to a new version on the project.
Args:
root_object (Base): The Speckle base object for the new version.
model_id (str): For now please use a `branchName`!
model_id (str): Id of model to create the new version on.
version_message (str): The message for the new version.
"""
branch = self.speckle_client.branch.get(
self.automation_run_data.project_id, model_name, 1
)
if isinstance(branch, Branch):
if not branch.id:
raise ValueError("Cannot use the branch without its id")
matching_trigger = [
t
for t in self.automation_run_data.triggers
if t.payload.model_id == branch.id
]
if matching_trigger:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {matching_trigger[0].payload.model_id}"
)
model_id = branch.id
else:
# we just check if it exists
branch_create = self.speckle_client.branch.create(
self.automation_run_data.project_id,
model_name,
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}"
)
if isinstance(branch_create, Exception):
raise branch_create
model_id = branch_create
root_object_id = operations.send(
root_object,
@@ -168,19 +176,17 @@ class AutomationContext:
use_default_cache=False,
)
version_id = self.speckle_client.commit.create(
stream_id=self.automation_run_data.project_id,
create_version_input = CreateVersionInput(
object_id=root_object_id,
branch_name=model_name,
model_id=model_id,
project_id=self.automation_run_data.project_id,
message=version_message,
source_application="SpeckleAutomate",
)
version = self.speckle_client.version.create(create_version_input)
if isinstance(version_id, SpeckleException):
raise version_id
self._automation_result.result_versions.append(version_id)
return model_id, version_id
self._automation_result.result_versions.append(version.id)
return version
@property
def context_view(self) -> Optional[str]:
+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 stringcase import camelcase
from pydantic.alias_generators import to_camel
class AutomateBase(BaseModel):
"""Use this class as a base model for automate related DTO."""
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
@@ -39,7 +39,7 @@ class AutomationRunData(BaseModel):
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
)
@@ -52,7 +52,7 @@ class TestAutomationRunData(BaseModel):
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
)
+6 -59
View File
@@ -1,7 +1,5 @@
import contextlib
from deprecated import deprecated
from specklepy.api.credentials import Account
from specklepy.api.resources import (
ActiveUserResource,
@@ -12,12 +10,6 @@ from specklepy.api.resources import (
ServerResource,
SubscriptionResource,
VersionResource,
branch,
commit,
object,
stream,
subscriptions,
user,
)
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
from specklepy.logging import metrics
@@ -36,6 +28,7 @@ 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
@@ -47,11 +40,12 @@ class SpeckleClient(CoreSpeckleClient):
account = get_default_account()
client.authenticate_with_account(account)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
# create a new project
input = ProjectCreateInput(name="a shiny new project")
project = self.project.create(input)
# use that stream id to get the stream from the server
new_stream = client.stream.get(id=new_stream_id)
# or, use a project id to get an existing project from the server
new_stream = client.project.get("abcdefghij")
```
"""
@@ -123,53 +117,6 @@ class SpeckleClient(CoreSpeckleClient):
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:
"""
-20
View File
@@ -1,35 +1,15 @@
# 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",
]
-20
View File
@@ -8,17 +8,6 @@ from specklepy.api.resources.current.project_resource import ProjectResource
from specklepy.api.resources.current.server_resource import ServerResource
from specklepy.api.resources.current.subscription_resource import SubscriptionResource
from specklepy.api.resources.current.version_resource import VersionResource
from specklepy.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
__all__ = [
"ActiveUserResource",
@@ -29,13 +18,4 @@ __all__ = [
"ServerResource",
"SubscriptionResource",
"VersionResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
@@ -1,7 +1,4 @@
from datetime import datetime
from typing import List, Optional, overload
from deprecated import deprecated
from typing import List, Optional
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.models import (
@@ -10,10 +7,6 @@ from specklepy.core.api.models import (
ResourceCollection,
User,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import ActiveUserResource as CoreResource
from specklepy.logging import metrics
@@ -35,40 +28,13 @@ 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,
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,
input: UserUpdateInput,
) -> User:
metrics.track(metrics.SDK, self.account, {"name": "Active User Update"})
if isinstance(input, UserUpdateInput):
return super()._update(input=input)
else:
return super()._update(
input=UserUpdateInput(
name=name,
company=company,
bio=bio,
avatar=avatar,
)
)
return super().update(input=input)
def get_projects(
self,
@@ -85,61 +51,3 @@ class ActiveUserResource(CoreResource):
metrics.SDK, self.account, {"name": "Active User Get Project Invites"}
)
return super().get_project_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Fetches collection the current authenticated user's activity
as filtered by given parameters
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
Args:
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): Filter results to a single action type.
before (Optional[datetime]): Latest cutoff for activity to include.
after (Optional[datetime]): Oldest cutoff for an activity to include.
cursor (Optional[datetime]): Timestamp cursor for pagination.
Returns:
Activity collection, filtered according to the provided parameters.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
return super().activity(limit, action_type, before, after, cursor)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Fetches all of the current user's pending stream invitations.
Returns:
List[PendingStreamCollaborator]: A list of pending stream invitations.
"""
metrics.track(
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
)
return super().get_all_pending_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Fetches a specific pending invite for the current user on a given stream.
Args:
stream_id (str): The ID of the stream to look for invites on.
token (Optional[str]): The token of the invite to look for (optional).
Returns:
Optional[PendingStreamCollaborator]: The invite for the given stream,
or None if not found.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
@@ -1,20 +1,11 @@
from datetime import datetime
from typing import List, Optional, Union
from deprecated import deprecated
from typing import Optional
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):
@@ -52,57 +43,3 @@ 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)
@@ -42,7 +42,7 @@ class VersionResource(CoreResource):
model_id, project_id, limit=limit, cursor=cursor, filter=filter
)
def create(self, input: CreateVersionInput) -> str:
def create(self, input: CreateVersionInput) -> Version:
metrics.track(metrics.SDK, self.account, {"name": "Version Create"})
return super().create(input)
@@ -1,9 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import ActiveUserResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(ActiveUserResource):
"""Renamed to ActiveUserResource"""
@@ -1,108 +0,0 @@
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)
@@ -1,134 +0,0 @@
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)
@@ -1,63 +0,0 @@
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)
@@ -1,11 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import OtherUserResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to OtherUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(OtherUserResource):
"""
Renamed to OtherUserResource
"""
@@ -1,9 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import ServerResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ServerResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""Renamed to ServerResource"""
@@ -1,322 +0,0 @@
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)
@@ -1,107 +0,0 @@
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
@@ -1,153 +0,0 @@
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 stream, branch, commit, or object URL.
Construct a `StreamWrapper` with a URL of a model, version, or object.
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 any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# provide a url for a model, version, or object
wrapper = StreamWrapper("https://app.speckle.systems/projects/3073b96e86/models/0fe47c9dca@604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
+7 -68
View File
@@ -3,14 +3,12 @@ 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 import resources
from specklepy.core.api.credentials import Account, get_account_from_token
from specklepy.core.api.credentials import Account
from specklepy.core.api.resources import (
ActiveUserResource,
ModelResource,
@@ -20,12 +18,6 @@ from specklepy.core.api.resources import (
ServerResource,
SubscriptionResource,
VersionResource,
branch,
commit,
object,
stream,
subscriptions,
user,
)
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
@@ -44,6 +36,7 @@ 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
@@ -55,11 +48,12 @@ class SpeckleClient:
account = get_default_account()
client.authenticate_with_account(account)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
# create a new project
input = ProjectCreateInput(name="a shiny new project")
project = self.project.create(input)
# use that stream id to get the stream from the server
new_stream = client.stream.get(id=new_stream_id)
# or, use a project id to get an existing project from the server
new_stream = client.project.get("abcdefghij")
```
"""
@@ -123,23 +117,6 @@ 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.
@@ -251,41 +228,3 @@ class SpeckleClient:
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 as ex:
raise SpeckleException(
f"Method {name} is not supported by the SpeckleClient class"
) from ex
+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.movedFrom if acc.serverInfo.migration else None
acc.serverInfo.migration.moved_from 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):
+10 -10
View File
@@ -1,26 +1,26 @@
from typing import Optional, Sequence
from pydantic import BaseModel
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
class CreateModelInput(BaseModel):
class CreateModelInput(GraphQLBaseModel):
name: str
description: Optional[str] = None
projectId: str
project_id: str
class DeleteModelInput(BaseModel):
class DeleteModelInput(GraphQLBaseModel):
id: str
projectId: str
project_id: str
class UpdateModelInput(BaseModel):
class UpdateModelInput(GraphQLBaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
projectId: str
project_id: str
class ModelVersionsFilter(BaseModel):
priorityIds: Sequence[str]
priorityIdsOnly: Optional[bool] = None
class ModelVersionsFilter(GraphQLBaseModel):
priority_ids: Sequence[str]
priority_ids_only: Optional[bool] = None
+15 -16
View File
@@ -1,47 +1,46 @@
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(BaseModel):
class ProjectCreateInput(GraphQLBaseModel):
name: Optional[str]
description: Optional[str]
visibility: Optional[ProjectVisibility]
class ProjectInviteCreateInput(BaseModel):
class ProjectInviteCreateInput(GraphQLBaseModel):
email: Optional[str]
role: Optional[str]
serverRole: Optional[str]
server_role: Optional[str]
userId: Optional[str]
class ProjectInviteUseInput(BaseModel):
class ProjectInviteUseInput(GraphQLBaseModel):
accept: bool
projectId: str
project_id: str
token: str
class ProjectModelsFilter(BaseModel):
class ProjectModelsFilter(GraphQLBaseModel):
contributors: Optional[Sequence[str]] = None
excludeIds: Optional[Sequence[str]] = None
exclude_ids: Optional[Sequence[str]] = None
ids: Optional[Sequence[str]] = None
onlyWithVersions: Optional[bool] = None
only_with_versions: Optional[bool] = None
search: Optional[str] = None
sourceApps: Optional[Sequence[str]] = None
source_apps: Optional[Sequence[str]] = None
class ProjectUpdateInput(BaseModel):
class ProjectUpdateInput(GraphQLBaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
allowPublicComments: Optional[bool] = None
allow_public_comments: Optional[bool] = None
visibility: Optional[ProjectVisibility] = None
class ProjectUpdateRoleInput(BaseModel):
userId: str
projectId: str
class ProjectUpdateRoleInput(GraphQLBaseModel):
user_id: str
project_id: str
role: Optional[str]
+4 -4
View File
@@ -1,15 +1,15 @@
from typing import Optional, Sequence
from pydantic import BaseModel
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
class UserUpdateInput(BaseModel):
class UserUpdateInput(GraphQLBaseModel):
avatar: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
name: Optional[str] = None
class UserProjectsFilter(BaseModel):
class UserProjectsFilter(GraphQLBaseModel):
search: str
onlyWithRoles: Optional[Sequence[str]] = None
only_with_roles: Optional[Sequence[str]] = None
+21 -21
View File
@@ -1,37 +1,37 @@
from typing import Optional, Sequence
from pydantic import BaseModel
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
class UpdateVersionInput(BaseModel):
versionId: str
projectId: str
class UpdateVersionInput(GraphQLBaseModel):
version_id: str
project_id: str
message: Optional[str]
class MoveVersionsInput(BaseModel):
targetModelName: str
versionIds: Sequence[str]
projectId: str
class MoveVersionsInput(GraphQLBaseModel):
target_model_name: str
version_ids: Sequence[str]
project_id: str
class DeleteVersionsInput(BaseModel):
versionIds: Sequence[str]
projectId: str
class DeleteVersionsInput(GraphQLBaseModel):
version_ids: Sequence[str]
project_id: str
class CreateVersionInput(BaseModel):
objectId: str
modelId: str
projectId: str
class CreateVersionInput(GraphQLBaseModel):
object_id: str
model_id: str
project_id: str
message: Optional[str] = None
sourceApplication: Optional[str] = "py"
totalChildrenCount: Optional[int] = None
source_application: Optional[str] = "py"
total_children_count: Optional[int] = None
parents: Optional[Sequence[str]] = None
class MarkReceivedVersionInput(BaseModel):
versionId: str
projectId: str
sourceApplication: str
class MarkReceivedVersionInput(GraphQLBaseModel):
version_id: str
project_id: str
source_application: str
message: Optional[str] = None
-22
View File
@@ -17,18 +17,6 @@ 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.subscription_messages import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
@@ -58,14 +46,4 @@ __all__ = [
"ProjectModelsUpdatedMessage",
"ProjectUpdatedMessage",
"ProjectVersionsUpdatedMessage",
"Collaborator",
"Commit",
"Commits",
"Object",
"Branch",
"Branches",
"Stream",
"Streams",
"Activity",
"ActivityCollection",
]
+56 -49
View File
@@ -1,15 +1,13 @@
from datetime import datetime
from typing import Generic, List, Optional, TypeVar
from pydantic import BaseModel
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.models.deprecated import Streams
from specklepy.core.api.models.graphql_base_model import GraphQLBaseModel
T = TypeVar("T")
class User(BaseModel):
class User(GraphQLBaseModel):
id: str
email: Optional[str] = None
name: str
@@ -18,7 +16,6 @@ class User(BaseModel):
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
streams: Optional["Streams"] = None
def __repr__(self):
return (
@@ -30,18 +27,18 @@ class User(BaseModel):
return self.__repr__()
class ResourceCollection(BaseModel, Generic[T]):
totalCount: int
class ResourceCollection(GraphQLBaseModel, Generic[T]):
total_count: int
items: List[T]
cursor: Optional[str] = None
class ServerMigration(BaseModel):
movedFrom: Optional[str]
movedTo: Optional[str]
class ServerMigration(GraphQLBaseModel):
moved_from: Optional[str]
moved_to: Optional[str]
class AuthStrategy(BaseModel):
class AuthStrategy(GraphQLBaseModel):
color: Optional[str]
icon: str
id: str
@@ -49,24 +46,24 @@ class AuthStrategy(BaseModel):
url: str
class ServerConfiguration(BaseModel):
blobSizeLimitBytes: int
objectMultipartUploadSizeLimitBytes: int
objectSizeLimitBytes: int
class ServerConfiguration(GraphQLBaseModel):
blob_size_limit_bytes: int
object_multipart_upload_size_limit_bytes: int
object_size_limit_bytes: int
# Keeping this one all Optionals at the minute,
# because its used both as a deserialization model for GQL and Account Management
class ServerInfo(BaseModel):
class ServerInfo(GraphQLBaseModel):
name: Optional[str] = None
company: Optional[str] = None
url: Optional[str] = None
adminContact: Optional[str] = None
admin_contact: Optional[str] = None
description: Optional[str] = None
canonicalUrl: Optional[str] = None
canonical_url: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
auth_strategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
@@ -74,7 +71,7 @@ class ServerInfo(BaseModel):
# TODO separate gql model from account management model
class LimitedUser(BaseModel):
class LimitedUser(GraphQLBaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
@@ -85,24 +82,34 @@ class LimitedUser(BaseModel):
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(BaseModel):
class PendingStreamCollaborator(GraphQLBaseModel):
id: str
inviteId: str
streamId: Optional[str] = None
invite_id: str
stream_id: Optional[str] = None
projectId: str
streamName: Optional[str] = None
projectName: str
stream_name: Optional[str] = None
project_name: str
title: str
role: str
invitedBy: LimitedUser
invited_by: LimitedUser
user: Optional[LimitedUser] = None
token: Optional[str]
def __repr__(self):
return (
f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId:"
f" {self.streamId}, role: {self.role}, title: {self.title}, invitedBy:"
f"PendingStreamCollaborator( inviteId: {self.invite_id}, streamId:"
f" {self.stream_id}, role: {self.role}, title: {self.title}, invitedBy:"
f" {self.user.name if self.user else None})"
)
@@ -110,48 +117,48 @@ class PendingStreamCollaborator(BaseModel):
return self.__repr__()
class ProjectCollaborator(BaseModel):
class ProjectCollaborator(GraphQLBaseModel):
id: str
role: str
user: LimitedUser
class Version(BaseModel):
authorUser: Optional[LimitedUser]
createdAt: datetime
class Version(GraphQLBaseModel):
author_user: Optional[LimitedUser]
created_at: datetime
id: str
message: Optional[str]
previewUrl: str
referencedObject: str
sourceApplication: Optional[str]
preview_url: str
referenced_object: str
source_application: Optional[str]
class Model(BaseModel):
class Model(GraphQLBaseModel):
author: Optional[LimitedUser]
createdAt: datetime
created_at: datetime
description: Optional[str]
displayName: str
display_name: str
id: str
name: str
previewUrl: Optional[str]
updatedAt: datetime
preview_url: Optional[str]
updated_at: datetime
class ModelWithVersions(Model):
versions: ResourceCollection[Version]
class Project(BaseModel):
allowPublicComments: bool
createdAt: datetime
class Project(GraphQLBaseModel):
allow_public_comments: bool
created_at: datetime
description: Optional[str]
id: str
name: str
role: Optional[str]
sourceApps: List[str]
updatedAt: datetime
source_apps: List[str]
updated_at: datetime
visibility: ProjectVisibility
workspaceId: Optional[str]
workspace_id: Optional[str]
class ProjectWithModels(Project):
@@ -159,14 +166,14 @@ class ProjectWithModels(Project):
class ProjectWithTeam(Project):
invitedTeam: List[PendingStreamCollaborator]
invited_team: List[PendingStreamCollaborator]
team: List[ProjectCollaborator]
class ProjectCommentCollection(ResourceCollection[T], Generic[T]):
totalArchivedCount: int
total_archived_count: int
class UserSearchResultCollection(BaseModel):
class UserSearchResultCollection(GraphQLBaseModel):
items: List[LimitedUser]
cursor: Optional[str] = None
-147
View File
@@ -1,147 +0,0 @@
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__()
@@ -0,0 +1,17 @@
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,
)
@@ -1,7 +1,5 @@
from typing import Optional
from pydantic import BaseModel
from specklepy.core.api.enums import (
ProjectModelsUpdatedMessageType,
ProjectUpdatedMessageType,
@@ -9,28 +7,29 @@ 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(BaseModel):
class UserProjectsUpdatedMessage(GraphQLBaseModel):
id: str
type: UserProjectsUpdatedMessageType
project: Optional[Project]
class ProjectModelsUpdatedMessage(BaseModel):
class ProjectModelsUpdatedMessage(GraphQLBaseModel):
id: str
type: ProjectModelsUpdatedMessageType
model: Optional[Model]
class ProjectUpdatedMessage(BaseModel):
class ProjectUpdatedMessage(GraphQLBaseModel):
id: str
type: ProjectUpdatedMessageType
project: Optional[Project]
class ProjectVersionsUpdatedMessage(BaseModel):
class ProjectVersionsUpdatedMessage(GraphQLBaseModel):
id: str
type: ProjectVersionsUpdatedMessageType
modelId: Optional[str]
model_id: str
version: Optional[Version]
@@ -10,17 +10,6 @@ from specklepy.core.api.resources.current.subscription_resource import (
SubscriptionResource,
)
from specklepy.core.api.resources.current.version_resource import VersionResource
from specklepy.core.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
__all__ = [
"ActiveUserResource",
@@ -31,13 +20,4 @@ __all__ = [
"ServerResource",
"SubscriptionResource",
"VersionResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
@@ -1,21 +1,14 @@
from datetime import datetime, timezone
from typing import List, Optional, overload
from typing import List, Optional
from deprecated import deprecated
from gql import gql
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Project,
ResourceCollection,
User,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.core.api.responses import DataResponse
from specklepy.logging.exceptions import GraphQLException
@@ -67,7 +60,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!) {
@@ -87,46 +80,12 @@ class ActiveUserResource(ResourceBase):
"""
)
variables = {"input": input.model_dump(warnings="error")}
variables = {"input": input.model_dump(warnings="error", by_alias=True)}
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,
*,
@@ -162,7 +121,9 @@ class ActiveUserResource(ResourceBase):
variables = {
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error") if filter else None,
"filter": filter.model_dump(warnings="error", by_alias=True)
if filter
else None,
}
response = self.make_request_and_parse_response(
@@ -229,184 +190,3 @@ class ActiveUserResource(ResourceBase):
)
return response.data.data
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated user's
activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime} -- latest cutoff for activity
(ie: return all activity _before_ this time)
after {datetime} -- oldest cutoff for activity
(ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
query = gql(
"""
query UserActivity(
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
activeUser {
activity(
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount
cursor
items {
actionType
info
userId
streamId
resourceId
resourceType
message
time
}
}
}
}
"""
)
params = {
"limit": limit,
"action_type": action_type,
"before": before.astimezone(timezone.utc).isoformat() if before else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": cursor.astimezone(timezone.utc).isoformat() if cursor else cursor,
}
return self.make_request(
query=query,
params=params,
return_type=["activeUser", "activity"],
schema=ActivityCollection,
)
@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
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
projectId
projectName
title
role
invitedBy {
id
name
bio
company
avatar
verified
role
}
}
}
"""
)
return self.make_request(
query=query,
return_type="streamInvites",
schema=PendingStreamCollaborator,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""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
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,
)
@@ -138,7 +138,7 @@ class ModelResource(ResourceBase):
"versionsLimit": versions_limit,
"versionsCursor": versions_cursor,
"versionsFilter": (
versions_filter.model_dump(warnings="error")
versions_filter.model_dump(warnings="error", by_alias=True)
if versions_filter
else None
),
@@ -201,7 +201,9 @@ class ModelResource(ResourceBase):
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": (
models_filter.model_dump(warnings="error") if models_filter else None
models_filter.model_dump(warnings="error", by_alias=True)
if models_filter
else None
),
}
@@ -238,7 +240,7 @@ class ModelResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -256,7 +258,7 @@ class ModelResource(ResourceBase):
"""
)
variables = {"input": input.model_dump(warnings="error")}
variables = {"input": input.model_dump(warnings="error", by_alias=True)}
return self.make_request_and_parse_response(
DataResponse[DataResponse[bool]], QUERY, variables
@@ -291,7 +293,7 @@ class ModelResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -1,21 +1,13 @@
from datetime import datetime, timezone
from typing import List, Optional, Union
from typing import Optional
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"
@@ -130,124 +122,3 @@ 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,
)
@@ -103,7 +103,7 @@ class ProjectInviteResource(ResourceBase):
variables = {
"projectId": project_id,
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -124,7 +124,7 @@ class ProjectInviteResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -118,7 +118,9 @@ class ProjectResource(ResourceBase):
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": (
models_filter.model_dump(warnings="error") if models_filter else None
models_filter.model_dump(warnings="error", by_alias=True)
if models_filter
else None
),
}
@@ -218,7 +220,7 @@ class ProjectResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -248,7 +250,7 @@ class ProjectResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -337,7 +339,7 @@ class ProjectResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -61,10 +61,10 @@ class ServerResource(ResourceBase):
query=query, return_type="serverInfo", schema=ServerInfo
)
if isinstance(server_info, ServerInfo) and isinstance(
server_info.canonicalUrl, str
server_info.canonical_url, str
):
r = requests.get(
server_info.canonicalUrl, headers={"User-Agent": "specklepy SDK"}
server_info.canonical_url, headers={"User-Agent": "specklepy SDK"}
)
if "x-speckle-frontend-2" in r.headers:
server_info.frontend2 = True
@@ -117,7 +117,9 @@ class VersionResource(ResourceBase):
"modelId": model_id,
"limit": limit,
"cursor": cursor,
"filter": filter.model_dump(warnings="error") if filter else None,
"filter": (
filter.model_dump(warnings="error", by_alias=True) if filter else None
),
}
return self.make_request_and_parse_response(
@@ -126,26 +128,39 @@ class VersionResource(ResourceBase):
variables,
).data.data.data
def create(self, input: CreateVersionInput) -> str:
def create(self, input: CreateVersionInput) -> Version:
QUERY = gql(
"""
mutation Create($input: CreateVersionInput!) {
data:versionMutations {
data:create(input: $input) {
data:id
id
referencedObject
message
sourceApplication
createdAt
previewUrl
authorUser {
id
name
bio
company
verified
role
avatar
}
}
}
}
"""
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
DataResponse[DataResponse[DataResponse[str]]], QUERY, variables
).data.data.data
DataResponse[DataResponse[Version]], QUERY, variables
).data.data
def update(self, input: UpdateVersionInput) -> Version:
QUERY = gql(
@@ -174,7 +189,7 @@ class VersionResource(ResourceBase):
"""
)
variables = {"input": input.model_dump(warnings="error")}
variables = {"input": input.model_dump(warnings="error", by_alias=True)}
return self.make_request_and_parse_response(
DataResponse[DataResponse[Version]], QUERY, variables
@@ -194,7 +209,7 @@ class VersionResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -213,7 +228,7 @@ class VersionResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -232,7 +247,7 @@ class VersionResource(ResourceBase):
)
variables = {
"input": input.model_dump(warnings="error"),
"input": input.model_dump(warnings="error", by_alias=True),
}
return self.make_request_and_parse_response(
@@ -1,15 +0,0 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import ActiveUserResource
@deprecated(
reason="Class renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION
)
class Resource(ActiveUserResource):
"""
Class renamed to ActiveUserResource
"""
pass
@@ -1,235 +0,0 @@
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
)
@@ -1,252 +0,0 @@
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
@@ -1,92 +0,0 @@
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
)
@@ -1,15 +0,0 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import OtherUserResource
@deprecated(
reason="Class renamed to OtherUserResource", version=FE1_DEPRECATION_VERSION
)
class Resource(OtherUserResource):
"""
Class renamed to OtherUserResource
"""
pass
@@ -1,11 +0,0 @@
from deprecated import deprecated
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
from specklepy.core.api.resources import ServerResource
NAME = "server"
@deprecated(reason="Renamed to ServerResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""API Access class for the server"""
@@ -1,785 +0,0 @@
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!) {
activeUser {
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=["activeUser", "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,
)
@@ -1,144 +0,0 @@
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
@@ -1,325 +0,0 @@
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,
)
+4 -4
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 stream, branch, commit, or object URL.
Construct a `StreamWrapper` with a URL of a model, version, or object.
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 any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# provide a url for a model, version, or object
wrapper = StreamWrapper("https://app.speckle.systems/projects/3073b96e86/models/0fe47c9dca@604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
@@ -163,7 +163,7 @@ class StreamWrapper:
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no {key_stream} ",
f"Cannot parse {url} into a stream wrapper class - no {key_stream} "
"id found.",
)
+2 -1
View File
@@ -1,6 +1,7 @@
from .data_objects import DataObject, QgisObject
from .data_objects import Base, DataObject, QgisObject
__all__ = [
"Base",
"DataObject",
"QgisObject",
]
+2 -2
View File
@@ -17,7 +17,7 @@ from typing import (
)
from warnings import warn
from stringcase import pascalcase
from pydantic.alias_generators import to_pascal
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.memory import MemoryTransport
@@ -147,7 +147,7 @@ class _RegisteringBase:
# convert the module names to PascalCase to match c# namespace naming convention
# also drop specklepy from the beginning
namespace = ".".join(
pascalcase(m)
to_pascal(m)
for m in filter(lambda name: name != "specklepy", cls.__module__.split("."))
)
return f"{namespace}.{cls.__name__}"
+2 -2
View File
@@ -1,8 +1,9 @@
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
from specklepy.logging.exceptions import SpeckleException
@dataclass(kw_only=True)
@@ -12,7 +13,6 @@ class DataObject(
speckle_type="Objects.Data.DataObject",
detachable={"displayValue"},
):
name: str
properties: Dict[str, object]
displayValue: List[Base]
+28 -8
View File
@@ -1,10 +1,20 @@
from specklepy.objects.geometry.arc import Arc
from specklepy.objects.geometry.line import Line
from specklepy.objects.geometry.mesh import Mesh
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.geometry.vector import Vector
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__ = [
@@ -14,5 +24,15 @@ __all__ = [
"Plane",
"Point",
"Polyline",
"Vector"
"Region",
"Vector",
"Box",
"Circle",
"ControlPoint",
"Ellipse",
"PointCloud",
"Polycurve",
"Spiral",
"Surface",
"Curve",
]
+6 -4
View File
@@ -23,8 +23,9 @@ class Arc(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Arc"):
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)))
angle = (2 * math.asin(start_to_mid / (2 * r))) + (
2 * math.asin(mid_to_end / (2 * r))
)
return r * angle
@property
@@ -32,5 +33,6 @@ class Arc(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Arc"):
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)))
return (2 * math.asin(start_to_mid / (2 * r))) + (
2 * math.asin(mid_to_end / (2 * r))
)
+40
View File
@@ -0,0 +1,40 @@
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
@@ -0,0 +1,35 @@
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
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,58 @@
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
@@ -0,0 +1,34 @@
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
+1 -6
View File
@@ -6,12 +6,7 @@ from specklepy.objects.interfaces import ICurve, IHasUnits
@dataclass(kw_only=True)
class Line(
Base,
IHasUnits,
ICurve,
speckle_type="Objects.Geometry.Line"
):
class Line(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Line"):
start: Point
end: Point
+10 -10
View File
@@ -23,8 +23,10 @@ class Mesh(
serialize_ignore={"vertices_count", "texture_coordinates_count"},
):
"""
a 3D mesh consisting of vertices and faces with optional colors and texture coordinates
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)
@@ -34,7 +36,6 @@ class Mesh(
return (
f"{self.__class__.__name__}("
f"vertices: {self.vertices_count}, "
f"faces: {self.faces_count}, "
f"units: {self.units}, "
f"has_colors: {len(self.colors) > 0}, "
f"has_texture_coords: {len(self.textureCoordinates) > 0})"
@@ -48,8 +49,8 @@ class Mesh(
if len(self.vertices) % 3 != 0:
raise ValueError(
f"Invalid vertices list: length ({len(
self.vertices)}) must be a multiple of 3"
f"Invalid vertices list: length {len(self.vertices)} "
f"must be a multiple of 3"
)
return len(self.vertices) // 3
@@ -62,19 +63,19 @@ class Mesh(
@property
def area(self) -> float:
return self.__dict__.get('_area', 0.0)
return self.__dict__.get("_area", 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__['_area'] = value
self.__dict__["_area"] = value
@property
def volume(self) -> float:
return self.__dict__.get('_volume', 0.0)
return self.__dict__.get("_volume", 0.0)
@volume.setter
def volume(self, value: float) -> None:
self.__dict__['_volume'] = value
self.__dict__["_volume"] = value
def calculate_area(self) -> float:
"""
@@ -180,8 +181,7 @@ class Mesh(
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")
raise IndexError(f"Vertex index {vertex_index} out of range")
vertices.append(self.get_point(vertex_index))
return vertices
+7 -1
View File
@@ -15,7 +15,13 @@ class Point(Base, IHasUnits, speckle_type="Objects.Geometry.Point"):
z: float
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, units: {self.units})"
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:
"""
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,97 @@
from dataclasses import dataclass, field
from typing import List
from specklepy.objects.base import Base
from specklepy.objects.geometry.line import Line
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits
@dataclass(kw_only=True)
class Polycurve(
Base, IHasUnits, ICurve, IHasArea, speckle_type="Objects.Geometry.Polycurve"
):
"""
a curve that is comprised of multiple curves connected
"""
segments: List[ICurve] = field(default_factory=list)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"segments: {len(self.segments)}, "
f"closed: {self.is_closed()}, "
f"units: {self.units})"
)
def is_closed(self, tolerance: float = 1e-6) -> bool:
"""
checks if the polycurve is closed
(comparing start of first segment to end of last segment)
"""
if len(self.segments) < 1:
return False
first_segment = self.segments[0]
last_segment = self.segments[-1]
if not (hasattr(first_segment, "start") and hasattr(last_segment, "end")):
return False
start_pt = first_segment.start
end_pt = last_segment.end
if not (isinstance(start_pt, Point) and isinstance(end_pt, Point)):
return False
return start_pt.distance_to(end_pt) <= tolerance
@property
def length(self) -> float:
return self.__dict__.get("_length", 0.0)
@length.setter
def length(self, value: float) -> None:
self.__dict__["_length"] = value
def calculate_length(self) -> float:
"""
calculate total length of all segments
"""
total_length = 0.0
for segment in self.segments:
if hasattr(segment, "length"):
total_length += segment.length
return total_length
@property
def area(self) -> float:
return self.__dict__.get("_area", 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__["_area"] = value
@classmethod
def from_polyline(cls, polyline: Polyline) -> "Polycurve":
"""
constructs a new polycurve instance from an existing polyline curve
"""
polycurve = cls(units=polyline.units)
points = polyline.get_points()
for i in range(len(points) - 1):
line = Line(start=points[i], end=points[i + 1], units=polyline.units)
polycurve.segments.append(line)
if polyline.is_closed():
line = Line(start=points[-1], end=points[0], units=polyline.units)
polycurve.segments.append(line)
if hasattr(polyline, "_length"):
polycurve.length = polyline.length
if hasattr(polyline, "_area"):
polycurve.area = polyline.area
return polycurve
+6 -11
View File
@@ -11,6 +11,7 @@ class Polyline(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Polyline"
"""
a polyline curve, defined by a set of vertices.
"""
value: List[float]
def __repr__(self) -> str:
@@ -25,26 +26,20 @@ class Polyline(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Polyline"
# compare first and last points
start = Point(
x=self.value[0],
y=self.value[1],
z=self.value[2],
units=self.units
x=self.value[0], y=self.value[1], z=self.value[2], units=self.units
)
end = Point(
x=self.value[-3],
y=self.value[-2],
z=self.value[-1],
units=self.units
x=self.value[-3], y=self.value[-2], z=self.value[-1], units=self.units
)
return start.distance_to(end) <= tolerance
@property
def length(self) -> float:
return self.__dict__.get('_length', 0.0)
return self.__dict__.get("_length", 0.0)
@length.setter
def length(self, value: float) -> None:
self.__dict__['_length'] = value
self.__dict__["_length"] = value
def calculate_length(self) -> float:
points = self.get_points()
@@ -70,7 +65,7 @@ class Polyline(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Polyline"
x=self.value[i],
y=self.value[i + 1],
z=self.value[i + 2],
units=self.units
units=self.units,
)
points.append(point)
return points
+60
View File
@@ -0,0 +1,60 @@
from dataclasses import dataclass, field
from typing import List
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.objects.geometry.box import Box
from specklepy.objects.geometry.mesh import Mesh
from specklepy.objects.interfaces import ICurve, IDisplayValue, IHasArea, IHasUnits
@dataclass(kw_only=True)
class Region(
Base,
IHasArea,
IDisplayValue[List[Mesh]],
IHasUnits,
speckle_type="Objects.Geometry.Region",
detachable={"displayValue"},
):
"""
Flat shape, defined by an outer boundary and inner loops.
"""
boundary: ICurve
innerLoops: List[ICurve]
hasHatchPattern: bool
bbox: Box | None = None
# unlike C#, constructor will require displayValue, even if it's empty
displayValue: List[Mesh]
_displayValue: List[Mesh] = field(repr=False, init=False)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"units: {self.units}, "
f"has_hatch_pattern: {self.hasHatchPattern}, "
f"inner_loops: {len(self.innerLoops)})"
)
@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 displayValue(self) -> List[Mesh]:
print(self._displayValue)
return self._displayValue
@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)}"
)
+49
View File
@@ -0,0 +1,49 @@
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.geometry.vector import Vector
from specklepy.objects.interfaces import ICurve, IHasArea, IHasUnits
@dataclass(kw_only=True)
class Spiral(Base, IHasUnits, ICurve, IHasArea, speckle_type="Objects.Geometry.Spiral"):
"""
a spiral
"""
start_point: Point
end_point: Point
plane: Plane # plane with origin at spiral center
turns: float # total angle of spiral.
pitch: float
pitch_axis: Vector
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"start_point: {self.start_point}, "
f"end_point: {self.end_point}, "
f"plane: {self.plane}, "
f"turns: {self.turns}, "
f"pitch: {self.pitch}, "
f"pitch_axis: {self.pitch_axis}, "
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
+65
View File
@@ -0,0 +1,65 @@
from dataclasses import dataclass
from typing import List
from specklepy.objects.base import Base
from specklepy.objects.geometry.control_point import ControlPoint
from specklepy.objects.interfaces import IHasArea, IHasUnits
from specklepy.objects.primitive import Interval
@dataclass(kw_only=True)
class Surface(Base, IHasArea, IHasUnits, speckle_type="Objects.Geometry.Surface"):
"""
a surface in nurbs form
"""
degreeU: int
degreeV: int
rational: bool
pointData: List[float]
countU: int
countV: int
knotsU: List[float]
knotsV: List[float]
domainU: Interval
domainV: Interval
closedU: bool
closedV: bool
@property
def area(self) -> float:
return self.__dict__.get("_area", 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__["_area"] = value
def get_control_points(self) -> List:
"""
gets the control points of this surface
"""
matrix = [[] for _ in range(self.countU)]
for i in range(0, len(self.pointData), 4):
u_index = i // (self.countV * 4)
x, y, z, w = self.pointData[i : i + 4]
matrix[u_index].append(
ControlPoint(x=x, y=y, z=z, weight=w, units=self.units)
)
return matrix
def set_control_points(self, value: List) -> None:
"""
sets the control points of this surface
"""
data = []
self.countU = len(value)
self.countV = len(value[0])
for row in value:
for pt in row:
data.extend([pt.x, pt.y, pt.z, pt.weight])
self.pointData = data
+11 -3
View File
@@ -5,7 +5,9 @@ from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class Vector(Base, IHasUnits, speckle_type="Objects.Geometry.Vector", serialize_ignore = {"length"}):
class Vector(
Base, IHasUnits, speckle_type="Objects.Geometry.Vector", serialize_ignore={"length"}
):
"""
a 3-dimensional vector
"""
@@ -15,8 +17,14 @@ class Vector(Base, IHasUnits, speckle_type="Objects.Geometry.Vector", serialize_
z: float
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, units: {self.units})"
return (
f"{self.__class__.__name__}("
f"x: {self.x}, "
f"y: {self.y}, "
f"z: {self.z}, "
f"units: {self.units})"
)
@property
def length(self) -> float:
return (self.x ** 2 + self.y ** 2 + self.z ** 2) ** 0.5
return (self.x**2 + self.y**2 + self.z**2) ** 0.5
+6 -7
View File
@@ -41,7 +41,6 @@ class IDisplayValue(Generic[T], metaclass=ABCMeta):
# field interfaces
@dataclass(kw_only=True)
class IHasUnits(metaclass=ABCMeta):
units: str | Units
_units: str = field(repr=False, init=False)
@@ -63,32 +62,32 @@ class IHasUnits(metaclass=ABCMeta):
@dataclass(kw_only=True)
class IHasArea(metaclass=ABCMeta):
_area: float = field(init=False, repr=False)
@property
@abstractmethod
def area(self) -> float:
return self._area
pass
@area.setter
def area(self, value: float):
if not isinstance(value, (int, float)):
if not isinstance(value, int | float):
raise ValueError(f"Area must be a number, got {type(value)}")
self._area = float(value)
@dataclass(kw_only=True)
class IHasVolume(metaclass=ABCMeta):
_volume: float = field(init=False, repr=False)
@property
@abstractmethod
def volume(self) -> float:
return self._volume
pass
@volume.setter
def volume(self, value: float):
if not isinstance(value, (int, float)):
if not isinstance(value, int | float):
raise ValueError(f"Volume must be a number, got {type(value)}")
self._volume = float(value)
+21
View File
@@ -0,0 +1,21 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
@dataclass(kw_only=True)
class RenderMaterial(
Base,
speckle_type="Objects.Other.RenderMaterial",
):
"""
Minimal physically based material DTO class. Based on references from
https://threejs.org/docs/index.html#api/en/materials/MeshStandardMaterial
"""
name: str
opacity: float = 1.0
metalness: float = 0.0
roughness: float = 1.0
diffuse: int # ARGB color as int
emissive: int = 0 # ARGB color as int, defaults to black
+4 -2
View File
@@ -4,7 +4,9 @@ from specklepy.objects.base import Base
@dataclass(kw_only=True)
class Interval(Base, speckle_type="Objects.Primitive.Interval", serialize_ignore={"length"}):
class Interval(
Base, speckle_type="Objects.Primitive.Interval", serialize_ignore={"length"}
):
start: float = 0.0
end: float = 0.0
@@ -13,7 +15,7 @@ class Interval(Base, speckle_type="Objects.Primitive.Interval", serialize_ignore
@property
def length(self) -> float:
abs(self.end - self.start)
return abs(self.end - self.start)
@classmethod
def unit_interval(cls) -> "Interval":
+23 -4
View File
@@ -3,12 +3,13 @@ from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IHasUnits
from specklepy.objects.other import RenderMaterial
@dataclass(kw_only=True)
class ColorProxy(
Base,
speckle_type="Models.Proxies.ColorProxy",
speckle_type="Speckle.Core.Models.Proxies.ColorProxy",
detachable={"objects"},
):
objects: List[str]
@@ -19,7 +20,7 @@ class ColorProxy(
@dataclass(kw_only=True)
class GroupProxy(
Base,
speckle_type="Models.Proxies.GroupProxy",
speckle_type="Speckle.Core.Models.Proxies.GroupProxy",
detachable={"objects"},
):
objects: List[str]
@@ -30,7 +31,7 @@ class GroupProxy(
class InstanceProxy(
Base,
IHasUnits,
speckle_type="Models.Proxies.InstanceProxy",
speckle_type="Speckle.Core.Models.Instances.InstanceProxy",
):
definition_id: str
transform: List[float]
@@ -40,9 +41,27 @@ class InstanceProxy(
@dataclass(kw_only=True)
class InstanceDefinitionProxy(
Base,
speckle_type="Models.Proxies.InstanceDefinitionProxy",
speckle_type="Speckle.Core.Models.Instances.InstanceDefinitionProxy",
detachable={"objects"},
):
objects: List[str]
max_depth: int
name: str
@dataclass(kw_only=True)
class RenderMaterialProxy(
Base,
speckle_type="Objects.Other.RenderMaterialProxy",
detachable={"objects"},
):
"""
used to store render material to object relationships in root collections
Args:
objects (list): the list of application ids of objects used by render material
value (RenderMaterial): the render material used by the objects
"""
objects: List[str]
value: RenderMaterial
-9
View File
@@ -1,9 +0,0 @@
from specklepy.objects.geometry import Point, Line
from specklepy.objects.models.units import Units
p_1 = Point(x=0, y=0, z=0, units=Units.m)
p_2 = Point(x=3, y=0, z=0, units=Units.m)
line = Line(start=p_1, end=p_2, units=Units.m)
line.length = line.calculate_length()
print(line.length)
+75 -64
View File
@@ -1,8 +1,13 @@
# ignoring "line too long" check from linter
# to match with SpeckleExceptions
# ruff: noqa: E501
import math
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import Arc, Plane, Point, Vector
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
@@ -27,8 +32,7 @@ def sample_plane():
ydir = Vector(x=0.0, y=1.0, z=0.0, units=Units.m)
plane = Plane(origin=origin, normal=normal,
xdir=xdir, ydir=ydir, units=Units.m)
plane = Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m)
return plane
@@ -37,11 +41,7 @@ def sample_plane():
def sample_arc(sample_points, sample_plane):
start, mid, end = sample_points
arc = Arc(
plane=sample_plane,
startPoint=start,
midPoint=mid,
endPoint=end,
units=Units.m
plane=sample_plane, startPoint=start, midPoint=mid, endPoint=end, units=Units.m
)
return arc
@@ -50,11 +50,7 @@ def sample_arc(sample_points, sample_plane):
def test_arc_creation(sample_points, sample_plane):
start, mid, end = sample_points
arc = Arc(
plane=sample_plane,
startPoint=start,
midPoint=mid,
endPoint=end,
units=Units.m
plane=sample_plane, startPoint=start, midPoint=mid, endPoint=end, units=Units.m
)
assert arc.startPoint == start
@@ -71,69 +67,84 @@ def test_arc_domain(sample_arc):
def test_arc_radius(sample_arc):
assert sample_arc.radius == pytest.approx(1.0)
def test_arc_length(sample_arc):
assert sample_arc.length == pytest.approx(math.pi)
def test_arc_units(sample_points, sample_plane):
@pytest.mark.parametrize(
"invalid_param, test_params, error_msg",
[
(
"plane",
{
"plane": "not a plane",
"startPoint": None,
"midPoint": None,
"endPoint": None,
},
"Cannot set 'Arc.plane':it expects type '<class 'specklepy.objects.geometry.plane.Plane'>',but received type 'str'",
),
(
"startPoint",
{
"plane": None,
"startPoint": "not a point",
"midPoint": None,
"endPoint": None,
},
"Cannot set 'Arc.startPoint':it expects type '<class 'specklepy.objects.geometry.point.Point'>',but received type 'str'",
),
(
"midPoint",
{
"plane": None,
"startPoint": None,
"midPoint": "not a point",
"endPoint": None,
},
"Cannot set 'Arc.midPoint':it expects type '<class 'specklepy.objects.geometry.point.Point'>',but received type 'str'",
),
(
"endPoint",
{
"plane": None,
"startPoint": None,
"midPoint": None,
"endPoint": "not a point",
},
"Cannot set 'Arc.endPoint':it expects type '<class 'specklepy.objects.geometry.point.Point'>',but received type 'str'",
),
],
)
def test_arc_inval(sample_points, sample_plane, invalid_param, test_params, error_msg):
start, mid, end = sample_points
if invalid_param != "plane":
test_params["plane"] = sample_plane
if invalid_param != "startPoint":
test_params["startPoint"] = start
if invalid_param != "midPoint":
test_params["midPoint"] = mid
if invalid_param != "endPoint":
test_params["endPoint"] = end
with pytest.raises(SpeckleException) as exc_info:
Arc(**test_params, units=Units.m)
assert str(exc_info.value) == f"SpeckleException: {error_msg}"
@pytest.mark.parametrize("new_units", ["mm", "cm", "in"])
def test_arc_units(sample_points, sample_plane, new_units):
start, mid, end = sample_points
arc = Arc(
plane=sample_plane,
startPoint=start,
midPoint=mid,
endPoint=end,
units=Units.m
plane=sample_plane, startPoint=start, midPoint=mid, endPoint=end, units=Units.m
)
assert arc.units == Units.m.value
arc.units = "mm"
assert arc.units == "mm"
def test_arc_invalid_construction(sample_points, sample_plane):
start, mid, end = sample_points
with pytest.raises(Exception):
Arc(
plane="not a plane",
startPoint=start,
midPoint=mid,
endPoint=end,
units=Units.m
)
with pytest.raises(Exception):
Arc(
plane=sample_plane,
startPoint="not a point",
midPoint=mid,
endPoint=end,
units=Units.m
)
with pytest.raises(Exception):
Arc(
plane=sample_plane,
startPoint=start,
midPoint="not a point",
endPoint=end,
units=Units.m
)
with pytest.raises(Exception):
Arc(
plane=sample_plane,
startPoint=start,
midPoint=mid,
endPoint="not a point",
units=Units.m
)
arc.units = new_units
assert arc.units == new_units
def test_arc_serialization(sample_arc):
+130
View File
@@ -0,0 +1,130 @@
# ignoring "line too long" check from linter
# to match with SpeckleExceptions
# ruff: noqa: E501
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import Box, Plane, Point, Vector
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
@pytest.fixture
def sample_plane():
origin = Point(x=0.0, y=0.0, z=0.0, units=Units.m)
normal = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
xdir = Vector(x=1.0, y=0.0, z=0.0, units=Units.m)
ydir = Vector(x=0.0, y=1.0, z=0.0, units=Units.m)
return Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m)
@pytest.fixture
def sample_intervals():
return (
Interval(start=0.0, end=1.0),
Interval(start=0.0, end=1.0),
Interval(start=0.0, end=1.0),
)
@pytest.fixture
def sample_box(sample_plane, sample_intervals):
xsize, ysize, zsize = sample_intervals
return Box(
basePlane=sample_plane, xSize=xsize, ySize=ysize, zSize=zsize, units=Units.m
)
def test_box_creation(sample_plane, sample_intervals):
xsize, ysize, zsize = sample_intervals
box = Box(
basePlane=sample_plane, xSize=xsize, ySize=ysize, zSize=zsize, units=Units.m
)
assert box.basePlane == sample_plane
assert box.xSize == xsize
assert box.ySize == ysize
assert box.zSize == zsize
assert box.units == Units.m.value
@pytest.mark.parametrize("expected_area", [6.0]) # 6 faces, each 1x1
def test_box_area(sample_box, expected_area):
sample_box.area = sample_box.area
assert sample_box.area == pytest.approx(expected_area)
@pytest.mark.parametrize("expected_volume", [1.0]) # 1x1x1 cube
def test_box_volume(sample_box, expected_volume):
sample_box.volume = sample_box.volume
assert sample_box.volume == pytest.approx(expected_volume)
@pytest.mark.parametrize("new_units", ["mm", "cm", "in"])
def test_box_units(sample_plane, sample_intervals, new_units):
xsize, ysize, zsize = sample_intervals
box = Box(
basePlane=sample_plane, xSize=xsize, ySize=ysize, zSize=zsize, units=Units.m
)
assert box.units == Units.m.value
box.units = new_units
assert box.units == new_units
@pytest.mark.parametrize(
"invalid_param, test_params, error_msg",
[
(
"basePlane",
{"basePlane": "not a plane", "xSize": None, "ySize": None, "zSize": None},
"Cannot set 'Box.basePlane':it expects type '<class 'specklepy.objects.geometry.plane.Plane'>',but received type 'str'",
),
(
"xSize",
{
"basePlane": None,
"xSize": "not an interval",
"ySize": None,
"zSize": None,
},
"Cannot set 'Box.xSize':it expects type '<class 'specklepy.objects.primitive.Interval'>',but received type 'str'",
),
],
)
def test_box_inval(
sample_plane, sample_intervals, invalid_param, test_params, error_msg
):
xsize, ysize, zsize = sample_intervals
if invalid_param != "basePlane":
test_params["basePlane"] = sample_plane
if invalid_param != "xSize":
test_params["xSize"] = xsize
if invalid_param != "ySize":
test_params["ySize"] = ysize
if invalid_param != "zSize":
test_params["zSize"] = zsize
with pytest.raises(SpeckleException) as exc_info:
Box(**test_params, units=Units.m)
assert str(exc_info.value) == f"SpeckleException: {error_msg}"
def test_box_serialization(sample_box):
serialized = serialize(sample_box)
deserialized = deserialize(serialized)
assert deserialized.basePlane.origin.x == sample_box.basePlane.origin.x
assert deserialized.basePlane.origin.y == sample_box.basePlane.origin.y
assert deserialized.basePlane.origin.z == sample_box.basePlane.origin.z
assert deserialized.xSize.start == sample_box.xSize.start
assert deserialized.xSize.end == sample_box.xSize.end
assert deserialized.ySize.start == sample_box.ySize.start
assert deserialized.ySize.end == sample_box.ySize.end
assert deserialized.zSize.start == sample_box.zSize.start
assert deserialized.zSize.end == sample_box.zSize.end
assert deserialized.units == sample_box.units
+123
View File
@@ -0,0 +1,123 @@
# ignoring "line too long" check from linter
# to match with SpeckleExceptions
# ruff: noqa: E501
import math
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import Circle, Plane, Point, Vector
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
@pytest.fixture
def sample_plane():
origin = Point(x=0.0, y=0.0, z=0.0, units=Units.m)
normal = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
xdir = Vector(x=1.0, y=0.0, z=0.0, units=Units.m)
ydir = Vector(x=0.0, y=1.0, z=0.0, units=Units.m)
return Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m)
@pytest.fixture
def sample_center():
return Point(x=0.0, y=0.0, z=0.0, units=Units.m)
@pytest.fixture
def sample_circle(sample_plane, sample_center):
return Circle(plane=sample_plane, center=sample_center, radius=1.0, units=Units.m)
def test_circle_creation(sample_plane, sample_center):
circle = Circle(plane=sample_plane, center=sample_center, radius=1.0, units=Units.m)
assert circle.plane == sample_plane
assert circle.center == sample_center
assert circle.radius == 1.0
assert circle.units == Units.m.value
def test_circle_domain(sample_circle):
assert isinstance(sample_circle.domain, Interval)
assert sample_circle.domain.start == 0.0
assert sample_circle.domain.end == 1.0
@pytest.mark.parametrize(
"radius,expected_length", [(1.0, 2 * math.pi), (2.0, 4 * math.pi), (0.5, math.pi)]
)
def test_circle_length(sample_circle, radius, expected_length):
sample_circle.radius = radius
assert sample_circle.length == pytest.approx(expected_length)
@pytest.mark.parametrize(
"radius,expected_area", [(1.0, math.pi), (2.0, 4 * math.pi), (0.5, math.pi * 0.25)]
)
def test_circle_area(sample_circle, radius, expected_area):
sample_circle.radius = radius
assert sample_circle.area == pytest.approx(expected_area)
@pytest.mark.parametrize("new_units", ["mm", "cm", "in"])
def test_circle_units(sample_plane, sample_center, new_units):
circle = Circle(plane=sample_plane, center=sample_center, radius=1.0, units=Units.m)
assert circle.units == Units.m.value
circle.units = new_units
assert circle.units == new_units
@pytest.mark.parametrize(
"invalid_param, test_params, error_msg",
[
(
"plane",
{"plane": "not a plane", "center": None, "radius": 1.0},
"Cannot set 'Circle.plane':it expects type '<class 'specklepy.objects.geometry.plane.Plane'>',but received type 'str'",
),
(
"center",
{"plane": None, "center": "not a point", "radius": 1.0},
"Cannot set 'Circle.center':it expects type '<class 'specklepy.objects.geometry.point.Point'>',but received type 'str'",
),
(
"radius",
{"plane": None, "center": None, "radius": "not a number"},
"Cannot set 'Circle.radius':it expects type '<class 'float'>',but received type 'str'",
),
],
)
def test_circle_inval(
sample_plane, sample_center, invalid_param, test_params, error_msg
):
if invalid_param != "plane":
test_params["plane"] = sample_plane
if invalid_param != "center":
test_params["center"] = sample_center
with pytest.raises(SpeckleException) as exc_info:
Circle(**test_params, units=Units.m)
assert str(exc_info.value) == f"SpeckleException: {error_msg}"
def test_circle_serialization(sample_circle):
serialized = serialize(sample_circle)
deserialized = deserialize(serialized)
assert deserialized.plane.origin.x == sample_circle.plane.origin.x
assert deserialized.plane.origin.y == sample_circle.plane.origin.y
assert deserialized.plane.origin.z == sample_circle.plane.origin.z
assert deserialized.plane.normal.x == sample_circle.plane.normal.x
assert deserialized.plane.normal.y == sample_circle.plane.normal.y
assert deserialized.plane.normal.z == sample_circle.plane.normal.z
assert deserialized.center.x == sample_circle.center.x
assert deserialized.center.y == sample_circle.center.y
assert deserialized.center.z == sample_circle.center.z
assert deserialized.radius == sample_circle.radius
assert deserialized.units == sample_circle.units
@@ -0,0 +1,77 @@
# ignoring "line too long" check from linter
# to match with SpeckleExceptions
# ruff: noqa: E501
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import ControlPoint
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_control_point():
return ControlPoint(x=1.0, y=2.0, z=3.0, weight=1.0, units=Units.m)
@pytest.mark.parametrize(
"x,y,z,weight,expected_units", [(1.0, 2.0, 3.0, 1.0, Units.m.value)]
)
def test_control_point_creation(x, y, z, weight, expected_units):
control_point = ControlPoint(x=x, y=y, z=z, weight=weight, units=Units.m)
assert control_point.x == x
assert control_point.y == y
assert control_point.z == z
assert control_point.weight == weight
assert control_point.units == expected_units
@pytest.mark.parametrize("new_units", ["mm", "cm", "in"])
def test_control_point_units(new_units):
control_point = ControlPoint(x=1.0, y=2.0, z=3.0, weight=1.0, units=Units.m)
assert control_point.units == Units.m.value
control_point.units = new_units
assert control_point.units == new_units
@pytest.mark.parametrize(
"invalid_param, test_params, error_msg",
[
(
"x",
{"x": "not a number", "y": 2.0, "z": 3.0, "weight": 1.0},
"Cannot set 'ControlPoint.x':it expects type '<class 'float'>',but received type 'str'",
),
(
"y",
{"x": 1.0, "y": "not a number", "z": 3.0, "weight": 1.0},
"Cannot set 'ControlPoint.y':it expects type '<class 'float'>',but received type 'str'",
),
(
"z",
{"x": 1.0, "y": 2.0, "z": "not a number", "weight": 1.0},
"Cannot set 'ControlPoint.z':it expects type '<class 'float'>',but received type 'str'",
),
(
"weight",
{"x": 1.0, "y": 2.0, "z": 3.0, "weight": "not a number"},
"Cannot set 'ControlPoint.weight':it expects type '<class 'float'>',but received type 'str'",
),
],
)
def test_control_point_invalid_construction(invalid_param, test_params, error_msg):
with pytest.raises(SpeckleException) as exc_info:
ControlPoint(**test_params, units=Units.m)
assert str(exc_info.value) == f"SpeckleException: {error_msg}"
def test_control_point_serialization(sample_control_point):
serialized = serialize(sample_control_point)
deserialized = deserialize(serialized)
assert deserialized.x == sample_control_point.x
assert deserialized.y == sample_control_point.y
assert deserialized.z == sample_control_point.z
assert deserialized.weight == sample_control_point.weight
assert deserialized.units == sample_control_point.units
+158
View File
@@ -0,0 +1,158 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Curve, Plane, Point, Polyline, Vector
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_polyline():
"""
sample polyline
"""
return Polyline(value=[0, 0, 0, 1, 0, 0, 1, 1, 0], units=Units.m)
@pytest.fixture
def sample_plane():
"""
sample plane for bbox creation
"""
origin = Point(x=0, y=0, z=0, units=Units.m)
normal = Vector(x=0, y=0, z=1, units=Units.m)
xdir = Vector(x=1, y=0, z=0, units=Units.m)
ydir = Vector(x=0, y=1, z=0, units=Units.m)
return Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m)
@pytest.fixture
def sample_curve(sample_polyline):
"""
sample curve for testing
"""
return Curve(
degree=3,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0],
weights=[1, 1, 1, 1],
knots=[0, 0, 0, 0, 1, 1, 1, 1],
closed=False,
displayValue=sample_polyline,
units=Units.m,
)
def test_curve_creation(sample_polyline):
"""
test curve initialization
"""
curve = Curve(
degree=3,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0],
weights=[1, 1, 1, 1],
knots=[0, 0, 0, 0, 1, 1, 1, 1],
closed=False,
displayValue=sample_polyline,
units=Units.m,
)
assert curve.degree == 3
assert curve.periodic is False
assert curve.rational is False
assert curve.points == [0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0]
assert curve.weights == [1, 1, 1, 1]
assert curve.knots == [0, 0, 0, 0, 1, 1, 1, 1]
assert curve.closed is False
assert curve.units == Units.m.value
assert curve.displayValue == sample_polyline
def test_length_property(sample_polyline):
"""
test the length property setter and getter
"""
curve = Curve(
degree=1,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 0, 0],
weights=[1, 1],
knots=[0, 0, 1, 1],
closed=False,
displayValue=sample_polyline,
units=Units.m,
)
assert curve.length == 0.0
curve.length = 1.5
assert curve.length == 1.5
def test_area_property(sample_polyline):
"""
test the area property setter and getter
"""
polyline = Polyline(
value=[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0], units=Units.m
)
curve = Curve(
degree=1,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0],
weights=[1, 1, 1, 1, 1],
knots=[0, 0, 1, 2, 3, 4, 4],
closed=True,
displayValue=polyline,
units=Units.m,
)
assert curve.area == 0.0
curve.area = 1.0
assert curve.area == 1.0
def test_curve_serialization(sample_curve):
"""
test serialization and deserialization of the curve
"""
serialized = serialize(sample_curve)
deserialized = deserialize(serialized)
assert deserialized.degree == sample_curve.degree
assert deserialized.periodic == sample_curve.periodic
assert deserialized.rational == sample_curve.rational
assert deserialized.points == sample_curve.points
assert deserialized.weights == sample_curve.weights
assert deserialized.knots == sample_curve.knots
assert deserialized.closed == sample_curve.closed
assert deserialized.units == sample_curve.units
@pytest.mark.parametrize("new_units", ["mm", "cm", "in"])
def test_curve_units(sample_polyline, new_units):
"""
test changing units of a curve
"""
curve = Curve(
degree=3,
periodic=False,
rational=False,
points=[0, 0, 0, 1, 1, 0, 2, 0, 0, 3, 1, 0],
weights=[1, 1, 1, 1],
knots=[0, 0, 0, 0, 1, 1, 1, 1],
closed=False,
displayValue=sample_polyline,
units=Units.m,
)
assert curve.units == Units.m.value
curve.units = new_units
assert curve.units == new_units
+113
View File
@@ -0,0 +1,113 @@
# ignoring "line too long" check from linter
# to match with SpeckleExceptions
# ruff: noqa: E501
import math
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import Ellipse, Plane, Point, Vector
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
@pytest.fixture
def sample_plane():
origin = Point(x=0.0, y=0.0, z=0.0, units=Units.m)
normal = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
xdir = Vector(x=1.0, y=0.0, z=0.0, units=Units.m)
ydir = Vector(x=0.0, y=1.0, z=0.0, units=Units.m)
return Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m)
@pytest.fixture
def sample_ellipse(sample_plane):
return Ellipse(
plane=sample_plane, first_radius=2.0, second_radius=1.0, units=Units.m
)
def test_ellipse_creation(sample_plane):
ellipse = Ellipse(
plane=sample_plane, first_radius=2.0, second_radius=1.0, units=Units.m
)
assert ellipse.plane == sample_plane
assert ellipse.first_radius == 2.0
assert ellipse.second_radius == 1.0
assert ellipse.units == Units.m.value
def test_ellipse_domain(sample_ellipse):
assert isinstance(sample_ellipse.domain, Interval)
assert sample_ellipse.domain.start == 0.0
assert sample_ellipse.domain.end == 1.0
@pytest.mark.parametrize(
"area_value",
[
10.0,
math.pi * 2.0, # area for circle with radius 2
0.0,
],
)
def test_ellipse_area(sample_ellipse, area_value):
sample_ellipse.area = area_value
assert sample_ellipse.area == pytest.approx(area_value)
@pytest.mark.parametrize("new_units", ["mm", "cm", "in"])
def test_ellipse_units(sample_plane, new_units):
ellipse = Ellipse(
plane=sample_plane, first_radius=2.0, second_radius=1.0, units=Units.m
)
assert ellipse.units == Units.m.value
ellipse.units = new_units
assert ellipse.units == new_units
@pytest.mark.parametrize(
"invalid_param, test_params, error_msg",
[
(
"plane",
{"plane": "not a plane", "first_radius": 2.0, "second_radius": 1.0},
"Cannot set 'Ellipse.plane':it expects type '<class 'specklepy.objects.geometry.plane.Plane'>',but received type 'str'",
),
(
"first_radius",
{"plane": None, "first_radius": "not a number", "second_radius": 1.0},
"Cannot set 'Ellipse.first_radius':it expects type '<class 'float'>',but received type 'str'",
),
(
"second_radius",
{"plane": None, "first_radius": 2.0, "second_radius": "not number"},
"Cannot set 'Ellipse.second_radius':it expects type '<class 'float'>',but received type 'str'",
),
],
)
def test_ellipse_invalid(sample_plane, invalid_param, test_params, error_msg):
if invalid_param != "plane":
test_params["plane"] = sample_plane
with pytest.raises(SpeckleException) as exc_info:
Ellipse(**test_params, units=Units.m)
assert str(exc_info.value) == f"SpeckleException: {error_msg}"
def test_ellipse_serialization(sample_ellipse):
serialized = serialize(sample_ellipse)
deserialized = deserialize(serialized)
assert deserialized.plane.origin.x == sample_ellipse.plane.origin.x
assert deserialized.plane.origin.y == sample_ellipse.plane.origin.y
assert deserialized.plane.origin.z == sample_ellipse.plane.origin.z
assert deserialized.first_radius == sample_ellipse.first_radius
assert deserialized.second_radius == sample_ellipse.second_radius
assert deserialized.units == sample_ellipse.units
+39 -31
View File
@@ -1,6 +1,11 @@
# ignoring "line too long" check from linter
# to match with SpeckleExceptions
# ruff: noqa: E501
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import Line, Point
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
@@ -8,7 +13,6 @@ from specklepy.objects.primitive import Interval
@pytest.fixture
def sample_points():
p1 = Point(x=0.0, y=0.0, z=0.0, units=Units.m)
p2 = Point(x=3.0, y=4.0, z=0.0, units=Units.m)
return p1, p2
@@ -16,49 +20,66 @@ def sample_points():
@pytest.fixture
def sample_line(sample_points):
start, end = sample_points
line = Line(start=start, end=end, units=Units.m)
return line
return Line(start=start, end=end, units=Units.m)
def test_line_creation(sample_points):
start, end = sample_points
line = Line(start=start, end=end, units=Units.m)
assert line.start == start
assert line.end == end
assert line.units == Units.m.value
def test_line_domain(sample_line):
# Domain should be automatically initialized to unit interval by ICurve
assert isinstance(sample_line.domain, Interval)
assert sample_line.domain.start == 0.0
assert sample_line.domain.end == 1.0
def test_line_length(sample_line):
assert sample_line.length == 5.0
@pytest.mark.parametrize("expected_length", [5.0])
def test_line_length(sample_line, expected_length):
assert sample_line.length == expected_length
def test_line_units(sample_points):
@pytest.mark.parametrize("new_units", ["mm", "cm", "in"])
def test_line_units(sample_points, new_units):
start, end = sample_points
line = Line(start=start, end=end, units=Units.m)
assert line.units == Units.m.value
line.units = new_units
assert line.units == new_units
# Test setting units with string
line.units = "mm"
assert line.units == "mm"
@pytest.mark.parametrize(
"invalid_param, test_params, error_msg",
[
(
"start",
{"start": "not a point", "end": None},
"Cannot set 'Line.start':it expects type '<class 'specklepy.objects.geometry.point.Point'>',but received type 'str'",
),
(
"end",
{"start": None, "end": "not a point"},
"Cannot set 'Line.end':it expects type '<class 'specklepy.objects.geometry.point.Point'>',but received type 'str'",
),
],
)
def test_line_invalid(sample_points, invalid_param, test_params, error_msg):
start, end = sample_points
if invalid_param != "start":
test_params["start"] = start
if invalid_param != "end":
test_params["end"] = end
with pytest.raises(SpeckleException) as exc_info:
Line(**test_params, units=Units.m)
assert str(exc_info.value) == f"SpeckleException: {error_msg}"
def test_line_serialization(sample_line):
serialized = serialize(sample_line)
deserialized = deserialize(serialized)
@@ -71,16 +92,3 @@ def test_line_serialization(sample_line):
assert deserialized.units == sample_line.units
assert deserialized.domain.start == sample_line.domain.start
assert deserialized.domain.end == sample_line.domain.end
def test_line_invalid_construction():
"""Test error cases"""
p1 = Point(x=0.0, y=0.0, z=0.0, units=Units.m)
# Test with invalid start point
with pytest.raises(Exception):
Line(start="not a point", end=p1)
# Test with invalid end point
with pytest.raises(Exception):
Line(start=p1, end="not a point")
+104 -43
View File
@@ -8,77 +8,143 @@ from specklepy.objects.models.units import Units
@pytest.fixture
def cube_vertices():
return [
-0.5, -0.5, -0.5,
0.5, -0.5, -0.5,
0.5, 0.5, -0.5,
-0.5, 0.5, -0.5,
-0.5, -0.5, 0.5,
0.5, -0.5, 0.5,
0.5, 0.5, 0.5,
-0.5, 0.5, 0.5
-0.5,
-0.5,
-0.5,
0.5,
-0.5,
-0.5,
0.5,
0.5,
-0.5,
-0.5,
0.5,
-0.5,
-0.5,
-0.5,
0.5,
0.5,
-0.5,
0.5,
0.5,
0.5,
0.5,
-0.5,
0.5,
0.5,
]
@pytest.fixture
def cube_faces():
return [
4, 0, 3, 2, 1, # bottom (-z)
4, 4, 5, 6, 7, # top (+z)
4, 0, 1, 5, 4, # front (-y)
4, 3, 7, 6, 2, # back (+y)
4, 0, 4, 7, 3, # left (-x)
4, 1, 2, 6, 5 # right (+x)
4,
0,
3,
2,
1, # bottom (-z)
4,
4,
5,
6,
7, # top (+z)
4,
0,
1,
5,
4, # front (-y)
4,
3,
7,
6,
2, # back (+y)
4,
0,
4,
7,
3, # left (-x)
4,
1,
2,
6,
5, # right (+x)
]
@pytest.fixture
def cube_colors():
return [
255, 0, 0, 255, # red
0, 255, 0, 255, # green
0, 0, 255, 255, # blue
255, 255, 0, 255, # yellow
255, 0, 255, 255, # magenta
0, 255, 255, 255, # cyan
255, 255, 255, 255, # white
0, 0, 0, 255 # black
255,
0,
0,
255, # red
0,
255,
0,
255, # green
0,
0,
255,
255, # blue
255,
255,
0,
255, # yellow
255,
0,
255,
255, # magenta
0,
255,
255,
255, # cyan
255,
255,
255,
255, # white
0,
0,
0,
255, # black
]
@pytest.fixture
def cube_texture_coords():
return [
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0,
0.0, 0.0,
1.0, 0.0,
1.0, 1.0,
0.0, 1.0
0.0,
0.0,
1.0,
0.0,
1.0,
1.0,
0.0,
1.0,
0.0,
0.0,
1.0,
0.0,
1.0,
1.0,
0.0,
1.0,
]
@pytest.fixture
def sample_mesh(cube_vertices, cube_faces):
return Mesh(vertices=cube_vertices, faces=cube_faces, units=Units.m)
@pytest.fixture
def full_mesh(cube_vertices, cube_faces, cube_colors, cube_texture_coords):
return Mesh(
vertices=cube_vertices,
faces=cube_faces,
colors=cube_colors,
textureCoordinates=cube_texture_coords,
units=Units.m
units=Units.m,
)
@@ -101,7 +167,6 @@ def test_mesh_texture_coordinates_count(full_mesh):
def test_mesh_get_point(sample_mesh):
point = sample_mesh.get_point(0)
assert isinstance(point, Point)
assert point.x == -0.5
@@ -142,14 +207,12 @@ def test_mesh_is_closed(sample_mesh):
def test_mesh_area(sample_mesh):
calculated_area = sample_mesh.calculate_area()
sample_mesh.area = calculated_area
assert sample_mesh.area == pytest.approx(6.0)
def test_mesh_volume(sample_mesh):
calculated_volume = sample_mesh.calculate_volume()
sample_mesh.volume = calculated_volume
@@ -158,15 +221,13 @@ def test_mesh_volume(sample_mesh):
def test_mesh_invalid_vertices():
mesh = Mesh(vertices=[0.0, 0.0], faces=[3, 0, 1, 2], units=Units.m)
with pytest.raises(ValueError):
mesh.vertices_count
_ = mesh.vertices_count
def test_mesh_invalid_faces():
vertices = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0]
with pytest.raises(IndexError):
# Face references vertex index out of range
+97 -82
View File
@@ -1,117 +1,132 @@
from typing import Any, Tuple
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Plane, Point, Vector
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import Plane, Point, Spiral, Vector
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_point():
point = Point(x=1.0, y=2.0, z=3.0, units=Units.m)
return point
def sample_points() -> Tuple[Point, Point]:
return Point(x=0.0, y=0.0, z=0.0, units=Units.m), Point(
x=0.0, y=0.0, z=2.0, units=Units.m
)
@pytest.fixture
def sample_vectors():
def sample_plane() -> Plane:
origin = Point(x=0.0, y=0.0, z=0.0, units=Units.m)
normal = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
xdir = Vector(x=1.0, y=0.0, z=0.0, units=Units.m)
ydir = Vector(x=0.0, y=1.0, z=0.0, units=Units.m)
return normal, xdir, ydir
return Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m)
@pytest.fixture
def sample_plane(sample_point, sample_vectors):
normal, xdir, ydir = sample_vectors
plane = Plane(
origin=sample_point,
normal=normal,
xdir=xdir,
ydir=ydir,
units=Units.m
)
return plane
def test_plane_creation(sample_point, sample_vectors):
normal, xdir, ydir = sample_vectors
plane = Plane(
origin=sample_point,
normal=normal,
xdir=xdir,
ydir=ydir,
units=Units.m
def sample_spiral(sample_points: Tuple[Point, Point], sample_plane: Plane) -> Spiral:
start, end = sample_points
pitch_axis = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
return Spiral(
start_point=start,
end_point=end,
plane=sample_plane,
turns=2.0,
pitch=1.0,
pitch_axis=pitch_axis,
units=Units.m,
)
assert plane.origin == sample_point
assert plane.normal == normal
assert plane.xdir == xdir
assert plane.ydir == ydir
assert plane.units == Units.m.value
def test_plane_units(sample_point, sample_vectors):
normal, xdir, ydir = sample_vectors
plane = Plane(
origin=sample_point,
normal=normal,
xdir=xdir,
ydir=ydir,
units=Units.m
def test_spiral_creation(sample_points: Tuple[Point, Point], sample_plane: Plane):
start, end = sample_points
pitch_axis = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
spiral = Spiral(
start_point=start,
end_point=end,
plane=sample_plane,
turns=2.0,
pitch=1.0,
pitch_axis=pitch_axis,
units=Units.m,
)
assert plane.units == Units.m.value
plane.units = "mm"
assert plane.units == "mm"
assert spiral.start_point == start
assert spiral.end_point == end
assert spiral.plane == sample_plane
assert spiral.turns == 2.0
assert spiral.pitch == 1.0
assert spiral.pitch_axis == pitch_axis
assert spiral.units == Units.m.value
def test_plane_invalid_construction():
@pytest.mark.parametrize(
"invalid_param,invalid_value",
[
("start_point", "not a point"),
("end_point", "not a point"),
("plane", "not a plane"),
("turns", "not a number"),
],
)
def test_spiral_invalid_construction(
sample_points: Tuple[Point, Point],
sample_plane: Plane,
invalid_param: str,
invalid_value: Any,
):
start, end = sample_points
pitch_axis = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
point = Point(x=1.0, y=2.0, z=3.0, units=Units.m)
normal = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
xdir = Vector(x=1.0, y=0.0, z=0.0, units=Units.m)
ydir = Vector(x=0.0, y=1.0, z=0.0, units=Units.m)
valid_params = {
"start_point": start,
"end_point": end,
"plane": sample_plane,
"turns": 2.0,
"pitch": 1.0,
"pitch_axis": pitch_axis,
"units": Units.m,
}
with pytest.raises(Exception):
Plane(origin="not a point", normal=normal, xdir=xdir, ydir=ydir)
valid_params[invalid_param] = invalid_value
with pytest.raises(Exception):
Plane(origin=point, normal="not a vector", xdir=xdir, ydir=ydir)
with pytest.raises(Exception):
Plane(origin=point, normal=normal, xdir="not a vector", ydir=ydir)
# Test with invalid ydir vector
with pytest.raises(Exception):
Plane(origin=point, normal=normal, xdir=xdir, ydir="not a vector")
with pytest.raises(SpeckleException):
Spiral(**valid_params)
def test_plane_serialization(sample_plane):
@pytest.mark.parametrize("test_value", [10.0])
def test_spiral_length(sample_spiral: Spiral, test_value: float):
sample_spiral.length = test_value
assert sample_spiral.length == test_value
serialized = serialize(sample_plane)
@pytest.mark.parametrize("test_value", [15.0])
def test_spiral_area(sample_spiral: Spiral, test_value: float):
sample_spiral.area = test_value
assert sample_spiral.area == test_value
def test_spiral_serialization(sample_spiral: Spiral):
serialized = serialize(sample_spiral)
deserialized = deserialize(serialized)
# Check all properties are preserved
assert deserialized.origin.x == sample_plane.origin.x
assert deserialized.origin.y == sample_plane.origin.y
assert deserialized.origin.z == sample_plane.origin.z
assert deserialized.start_point.x == sample_spiral.start_point.x
assert deserialized.start_point.y == sample_spiral.start_point.y
assert deserialized.start_point.z == sample_spiral.start_point.z
assert deserialized.normal.x == sample_plane.normal.x
assert deserialized.normal.y == sample_plane.normal.y
assert deserialized.normal.z == sample_plane.normal.z
assert deserialized.end_point.x == sample_spiral.end_point.x
assert deserialized.end_point.y == sample_spiral.end_point.y
assert deserialized.end_point.z == sample_spiral.end_point.z
assert deserialized.xdir.x == sample_plane.xdir.x
assert deserialized.xdir.y == sample_plane.xdir.y
assert deserialized.xdir.z == sample_plane.xdir.z
assert deserialized.plane.origin.x == sample_spiral.plane.origin.x
assert deserialized.plane.origin.y == sample_spiral.plane.origin.y
assert deserialized.plane.origin.z == sample_spiral.plane.origin.z
assert deserialized.ydir.x == sample_plane.ydir.x
assert deserialized.ydir.y == sample_plane.ydir.y
assert deserialized.ydir.z == sample_plane.ydir.z
assert deserialized.turns == sample_spiral.turns
assert deserialized.pitch == sample_spiral.pitch
assert deserialized.pitch_axis.x == sample_spiral.pitch_axis.x
assert deserialized.pitch_axis.y == sample_spiral.pitch_axis.y
assert deserialized.pitch_axis.z == sample_spiral.pitch_axis.z
assert deserialized.units == sample_plane.units
assert deserialized.units == sample_spiral.units
+1 -1
View File
@@ -18,7 +18,7 @@ def test_point_distance_calculation():
p2 = Point(x=4.0, y=6.0, z=8.0, units=Units.m)
distance = p1.distance_to(p2)
expected = ((3.0**2 + 4.0**2 + 5.0**2) ** 0.5)
expected = (3.0**2 + 4.0**2 + 5.0**2) ** 0.5
assert distance == pytest.approx(expected)
with pytest.raises(TypeError):
@@ -0,0 +1,60 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Point, PointCloud
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_points():
return [
Point(x=0.0, y=0.0, z=0.0, units=Units.m),
Point(x=1.0, y=0.0, z=0.0, units=Units.m),
Point(x=0.0, y=1.0, z=0.0, units=Units.m),
Point(x=1.0, y=1.0, z=0.0, units=Units.m),
]
@pytest.fixture
def sample_point_cloud(sample_points):
return PointCloud(points=sample_points, units=Units.m)
def test_point_cloud_creation(sample_points):
point_cloud = PointCloud(points=sample_points, units=Units.m)
assert len(point_cloud.points) == 4
assert isinstance(point_cloud.points, list)
assert all(isinstance(p, Point) for p in point_cloud.points)
assert point_cloud.units == Units.m.value
def test_point_cloud_units(sample_points):
point_cloud = PointCloud(points=sample_points, units=Units.m)
assert point_cloud.units == Units.m.value
point_cloud.units = "mm"
assert point_cloud.units == "mm"
def test_point_cloud_empty_points():
point_cloud = PointCloud(points=[], units=Units.m)
assert len(point_cloud.points) == 0
assert isinstance(point_cloud.points, list)
def test_point_cloud_serialization(sample_point_cloud):
serialized = serialize(sample_point_cloud)
deserialized = deserialize(serialized)
assert len(deserialized.points) == len(sample_point_cloud.points)
for orig_point, deserial_point in zip(
sample_point_cloud.points, deserialized.points, strict=True
):
assert deserial_point.x == orig_point.x
assert deserial_point.y == orig_point.y
assert deserial_point.z == orig_point.z
assert deserial_point.units == orig_point.units
assert deserialized.units == sample_point_cloud.units
@@ -0,0 +1,108 @@
from typing import List, Tuple
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import Line, Point, Polycurve, Polyline
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_points() -> Tuple[Point, Point, Point]:
return (
Point(x=0.0, y=0.0, z=0.0, units=Units.m),
Point(x=1.0, y=0.0, z=0.0, units=Units.m),
Point(x=1.0, y=1.0, z=0.0, units=Units.m),
)
@pytest.fixture
def sample_lines(sample_points: Tuple[Point, Point, Point]) -> List[Line]:
p1, p2, p3 = sample_points
return [
Line(start=p1, end=p2, units=Units.m),
Line(start=p2, end=p3, units=Units.m),
]
@pytest.fixture
def sample_polycurve(sample_lines: List[Line]) -> Polycurve:
return Polycurve(segments=sample_lines, units=Units.m)
def test_polycurve_creation(sample_lines: List[Line]):
polycurve = Polycurve(segments=sample_lines, units=Units.m)
assert len(polycurve.segments) == 2
assert polycurve.units == Units.m.value
assert isinstance(polycurve.segments[0], Line)
def test_polycurve_is_closed(sample_points: Tuple[Point, Point, Point]):
p1, p2, p3 = sample_points
lines = [
Line(start=p1, end=p2, units=Units.m),
Line(start=p2, end=p3, units=Units.m),
Line(start=p3, end=p1, units=Units.m),
]
closed_polycurve = Polycurve(segments=lines, units=Units.m)
assert closed_polycurve.is_closed()
def test_polycurve_not_closed(sample_polycurve: Polycurve):
assert not sample_polycurve.is_closed()
@pytest.mark.parametrize("expected_length", [2.0])
def test_polycurve_length(sample_polycurve: Polycurve, expected_length: float):
sample_polycurve.length = sample_polycurve.calculate_length()
assert sample_polycurve.length == pytest.approx(expected_length)
@pytest.mark.parametrize(
"points,expected_segments,expected_closed",
[
([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0], 2, False),
(
[0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0],
4,
True,
),
],
)
def test_polycurve_from_polyline(
points: List[float], expected_segments: int, expected_closed: bool
):
polyline = Polyline(value=points, units=Units.m)
polycurve = Polycurve.from_polyline(polyline)
assert len(polycurve.segments) == expected_segments
assert polycurve.units == Units.m.value
assert isinstance(polycurve.segments[0], Line)
assert polycurve.is_closed() == expected_closed
def test_polycurve_serialization(sample_polycurve: Polycurve):
serialized = serialize(sample_polycurve)
deserialized = deserialize(serialized)
assert len(deserialized.segments) == len(sample_polycurve.segments)
assert deserialized.units == sample_polycurve.units
assert deserialized.segments[0].start.x == sample_polycurve.segments[0].start.x
assert deserialized.segments[0].start.y == sample_polycurve.segments[0].start.y
assert deserialized.segments[0].start.z == sample_polycurve.segments[0].start.z
assert deserialized.segments[0].end.x == sample_polycurve.segments[0].end.x
assert deserialized.segments[0].end.y == sample_polycurve.segments[0].end.y
assert deserialized.segments[0].end.z == sample_polycurve.segments[0].end.z
def test_polycurve_empty():
polycurve = Polycurve(segments=[], units=Units.m)
assert not polycurve.is_closed()
assert polycurve.calculate_length() == 0.0
def test_polycurve_invalid_segment():
with pytest.raises(SpeckleException):
Polycurve(segments=["not a curve"], units=Units.m)
+33 -16
View File
@@ -8,24 +8,40 @@ from specklepy.objects.primitive import Interval
@pytest.fixture
def open_square_coords():
return [
0.0, 0.0, 0.0, # point 1
1.0, 0.0, 0.0, # point 2
1.0, 1.0, 0.0, # point 3
0.0, 1.0, 0.0 # point 4
0.0,
0.0,
0.0, # point 1
1.0,
0.0,
0.0, # point 2
1.0,
1.0,
0.0, # point 3
0.0,
1.0,
0.0, # point 4
]
@pytest.fixture
def closed_square_coords():
return [
0.0, 0.0, 0.0, # point 1
1.0, 0.0, 0.0, # point 2
1.0, 1.0, 0.0, # point 3
0.0, 1.0, 0.0, # point 4
0.0, 0.0, 0.0 # point 5 (same as point 1)
0.0,
0.0,
0.0, # point 1
1.0,
0.0,
0.0, # point 2
1.0,
1.0,
0.0, # point 3
0.0,
1.0,
0.0, # point 4
0.0,
0.0,
0.0, # point 5 (same as point 1)
]
@@ -55,9 +71,11 @@ def test_polyline_is_closed(open_square_coords, closed_square_coords):
def test_polyline_is_closed_with_tolerance(open_square_coords):
almost_closed = open_square_coords + \
[0.0, 0.0, 0.001] # last point slightly above start
almost_closed = open_square_coords + [
0.0,
0.0,
0.001,
] # last point slightly above start
poly = Polyline(value=almost_closed, units=Units.m)
assert not poly.is_closed(tolerance=1e-6)
@@ -87,7 +105,7 @@ def test_polyline_get_points(sample_polyline):
Point(x=0.0, y=0.0, z=0.0, units=Units.m),
Point(x=1.0, y=0.0, z=0.0, units=Units.m),
Point(x=1.0, y=1.0, z=0.0, units=Units.m),
Point(x=0.0, y=1.0, z=0.0, units=Units.m)
Point(x=0.0, y=1.0, z=0.0, units=Units.m),
]
# Check coordinates match
@@ -98,7 +116,6 @@ def test_polyline_get_points(sample_polyline):
def test_polyline_invalid_coordinates():
invalid_coords = [0.0, 0.0, 0.0, 1.0, 1.0] # missing one coordinate
with pytest.raises(ValueError):
polyline = Polyline(value=invalid_coords, units=Units.m)
@@ -0,0 +1,84 @@
# ignoring "line too long" check from linter
# to match with SpeckleExceptions
# ruff: noqa: E501
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.geometry.region import Region
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_boundary():
return Polyline(
# possibly replace same start-end Polyline point with "closed" property
value=[-10, -10, 0, 10, -10, 0, 10, 10, 0, -10, 10, 0, -10, -10, 0],
units=Units.m,
)
@pytest.fixture
def sample_loop1():
return Polyline(
# possibly replace same start-end Polyline point with "closed" property
value=[-9, -9, 0, -5, -9, 0, -5, -5, 0, -9, -5, 0, -9, -9, 0],
units=Units.m,
)
@pytest.fixture
def sample_loop2():
return Polyline(
# possibly replace same start-end Polyline point with "closed" property
value=[5, 5, 0, 9, 5, 0, 9, 9, 0, 5, 9, 0, 5, 5, 0],
units=Units.m,
)
@pytest.fixture
def sample_region(sample_boundary, sample_loop1, sample_loop2):
return Region(
boundary=sample_boundary,
innerLoops=[sample_loop1, sample_loop2],
hasHatchPattern=True,
units=Units.m,
displayValue=[],
)
def test_region_creation(sample_boundary, sample_loop1, sample_loop2):
has_hatch_pattern = True
region = Region(
boundary=sample_boundary,
innerLoops=[sample_loop1, sample_loop2],
hasHatchPattern=has_hatch_pattern,
units=Units.m,
displayValue=[],
)
assert region.boundary == sample_boundary
assert region.innerLoops[0] == sample_loop1
assert region.innerLoops[1] == sample_loop2
assert region.hasHatchPattern == has_hatch_pattern
assert len(region.displayValue) == 0
assert region.units == Units.m.value
def test_region_serialization(sample_region):
serialized = serialize(sample_region)
deserialized = deserialize(serialized)
assert deserialized.hasHatchPattern == sample_region.hasHatchPattern
assert deserialized.units == sample_region.units
assert deserialized.boundary.length == sample_region.boundary.length
assert deserialized.boundary.domain.length == sample_region.boundary.domain.length
assert deserialized.boundary.domain.start == sample_region.boundary.domain.start
assert deserialized.boundary.domain.end == sample_region.boundary.domain.end
for i, loop in enumerate(sample_region.innerLoops):
assert deserialized.innerLoops[i].length == loop.length
assert deserialized.innerLoops[i].domain.length == loop.domain.length
assert deserialized.innerLoops[i].domain.start == loop.domain.start
assert deserialized.innerLoops[i].domain.end == loop.domain.end
+130
View File
@@ -0,0 +1,130 @@
from typing import Any, Tuple
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import Plane, Point, Spiral, Vector
from specklepy.objects.models.units import Units
@pytest.fixture
def sample_points() -> Tuple[Point, Point]:
return (
Point(x=0.0, y=0.0, z=0.0, units=Units.m),
Point(x=0.0, y=0.0, z=2.0, units=Units.m),
)
@pytest.fixture
def sample_plane() -> Plane:
origin = Point(x=0.0, y=0.0, z=0.0, units=Units.m)
normal = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
xdir = Vector(x=1.0, y=0.0, z=0.0, units=Units.m)
ydir = Vector(x=0.0, y=1.0, z=0.0, units=Units.m)
return Plane(origin=origin, normal=normal, xdir=xdir, ydir=ydir, units=Units.m)
@pytest.fixture
def sample_spiral(sample_points: Tuple[Point, Point], sample_plane: Plane) -> Spiral:
start, end = sample_points
pitch_axis = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
return Spiral(
start_point=start,
end_point=end,
plane=sample_plane,
turns=2.0,
pitch=1.0,
pitch_axis=pitch_axis,
units=Units.m,
)
@pytest.mark.parametrize("units", [Units.m])
def test_spiral_creation(
sample_points: Tuple[Point, Point], sample_plane: Plane, units: Units
):
start, end = sample_points
pitch_axis = Vector(x=0.0, y=0.0, z=1.0, units=units)
spiral = Spiral(
start_point=start,
end_point=end,
plane=sample_plane,
turns=2.0,
pitch=1.0,
pitch_axis=pitch_axis,
units=units,
)
assert spiral.start_point == start
assert spiral.end_point == end
assert spiral.plane == sample_plane
assert spiral.turns == 2.0
assert spiral.pitch == 1.0
assert spiral.pitch_axis == pitch_axis
assert spiral.units == units.value
@pytest.mark.parametrize(
"invalid_param,invalid_value",
[
("start_point", "not a point"),
("end_point", "not a point"),
("plane", "not a plane"),
("turns", "not a number"),
],
)
def test_spiral_invalid_construction(
sample_points: Tuple[Point, Point],
sample_plane: Plane,
invalid_param: str,
invalid_value: Any,
):
start, end = sample_points
pitch_axis = Vector(x=0.0, y=0.0, z=1.0, units=Units.m)
valid_params = {
"start_point": start,
"end_point": end,
"plane": sample_plane,
"turns": 2.0,
"pitch": 1.0,
"pitch_axis": pitch_axis,
"units": Units.m,
}
valid_params[invalid_param] = invalid_value
with pytest.raises(SpeckleException):
Spiral(**valid_params)
@pytest.mark.parametrize("test_value", [10.0])
def test_spiral_length(sample_spiral: Spiral, test_value: float):
sample_spiral.length = test_value
assert sample_spiral.length == test_value
@pytest.mark.parametrize("test_value", [15.0])
def test_spiral_area(sample_spiral: Spiral, test_value: float):
sample_spiral.area = test_value
assert sample_spiral.area == test_value
def test_spiral_serialization(sample_spiral: Spiral):
serialized = serialize(sample_spiral)
deserialized = deserialize(serialized)
assert deserialized.start_point.x == sample_spiral.start_point.x
assert deserialized.start_point.y == sample_spiral.start_point.y
assert deserialized.start_point.z == sample_spiral.start_point.z
assert deserialized.end_point.x == sample_spiral.end_point.x
assert deserialized.end_point.y == sample_spiral.end_point.y
assert deserialized.end_point.z == sample_spiral.end_point.z
assert deserialized.plane.origin.x == sample_spiral.plane.origin.x
assert deserialized.plane.origin.y == sample_spiral.plane.origin.y
assert deserialized.plane.origin.z == sample_spiral.plane.origin.z
assert deserialized.turns == sample_spiral.turns
assert deserialized.pitch == sample_spiral.pitch
assert deserialized.pitch_axis.x == sample_spiral.pitch_axis.x
assert deserialized.pitch_axis.y == sample_spiral.pitch_axis.y
assert deserialized.pitch_axis.z == sample_spiral.pitch_axis.z
assert deserialized.units == sample_spiral.units
+189
View File
@@ -0,0 +1,189 @@
from typing import Any, List, Tuple
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.geometry import ControlPoint, Surface
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
@pytest.fixture
def sample_intervals() -> Tuple[Interval, Interval]:
return (Interval(start=0.0, end=1.0), Interval(start=0.0, end=1.0))
@pytest.fixture
def sample_point_data() -> List[float]:
return [
0.0,
0.0,
0.0,
1.0, # point 1
1.0,
0.0,
0.0,
1.0, # point 2
0.0,
1.0,
0.0,
1.0, # point 3
1.0,
1.0,
0.0,
1.0, # point 4
]
@pytest.fixture
def sample_surface(
sample_intervals: Tuple[Interval, Interval], sample_point_data: List[float]
) -> Surface:
domain_u, domain_v = sample_intervals
return Surface(
degreeU=1,
degreeV=1,
rational=True,
pointData=sample_point_data,
countU=2,
countV=2,
knotsU=[0.0, 0.0, 1.0, 1.0],
knotsV=[0.0, 0.0, 1.0, 1.0],
domainU=domain_u,
domainV=domain_v,
closedU=False,
closedV=False,
units=Units.m,
)
@pytest.mark.parametrize("units", [Units.m])
def test_surface_creation(
sample_intervals: Tuple[Interval, Interval],
sample_point_data: List[float],
units: Units,
):
domain_u, domain_v = sample_intervals
surface = Surface(
degreeU=1,
degreeV=1,
rational=True,
pointData=sample_point_data,
countU=2,
countV=2,
knotsU=[0.0, 0.0, 1.0, 1.0],
knotsV=[0.0, 0.0, 1.0, 1.0],
domainU=domain_u,
domainV=domain_v,
closedU=False,
closedV=False,
units=units,
)
assert surface.degreeU == 1
assert surface.degreeV == 1
assert surface.rational
assert surface.pointData == sample_point_data
assert surface.countU == 2
assert surface.countV == 2
assert surface.knotsU == [0.0, 0.0, 1.0, 1.0]
assert surface.knotsV == [0.0, 0.0, 1.0, 1.0]
assert surface.domainU == domain_u
assert surface.domainV == domain_v
assert not surface.closedU
assert not surface.closedV
assert surface.units == units.value
@pytest.mark.parametrize("test_value", [1.0])
def test_surface_area(sample_surface: Surface, test_value: float):
sample_surface.area = test_value
assert sample_surface.area == test_value
def test_surface_get_control_points(sample_surface: Surface):
control_points = sample_surface.get_control_points()
assert len(control_points) == 2
assert len(control_points[0]) == 2
assert isinstance(control_points[0][0], ControlPoint)
assert control_points[0][0].x == 0.0
assert control_points[0][0].y == 0.0
assert control_points[0][0].z == 0.0
assert control_points[0][0].weight == 1.0
assert control_points[0][0].units == Units.m.value
@pytest.mark.parametrize("units", [Units.m])
def test_surface_set_control_points(sample_surface: Surface, units: Units):
control_points = [
[
ControlPoint(x=0.0, y=0.0, z=0.0, weight=1.0, units=units),
ControlPoint(x=1.0, y=0.0, z=0.0, weight=1.0, units=units),
],
[
ControlPoint(x=0.0, y=1.0, z=0.0, weight=1.0, units=units),
ControlPoint(x=1.0, y=1.0, z=0.0, weight=1.0, units=units),
],
]
sample_surface.set_control_points(control_points)
assert sample_surface.countU == 2
assert sample_surface.countV == 2
assert len(sample_surface.pointData) == 16
assert sample_surface.pointData[0:4] == [0.0, 0.0, 0.0, 1.0]
def test_surface_serialization(sample_surface: Surface):
serialized = serialize(sample_surface)
deserialized = deserialize(serialized)
assert deserialized.degreeU == sample_surface.degreeU
assert deserialized.degreeV == sample_surface.degreeV
assert deserialized.rational == sample_surface.rational
assert deserialized.pointData == sample_surface.pointData
assert deserialized.countU == sample_surface.countU
assert deserialized.countV == sample_surface.countV
assert deserialized.knotsU == sample_surface.knotsU
assert deserialized.knotsV == sample_surface.knotsV
assert deserialized.domainU.start == sample_surface.domainU.start
assert deserialized.domainU.end == sample_surface.domainU.end
assert deserialized.domainV.start == sample_surface.domainV.start
assert deserialized.domainV.end == sample_surface.domainV.end
assert deserialized.closedU == sample_surface.closedU
assert deserialized.closedV == sample_surface.closedV
assert deserialized.units == sample_surface.units
@pytest.mark.parametrize(
"invalid_param,invalid_value",
[("degreeU", "not a number")],
)
def test_surface_invalid_construction(
sample_intervals: Tuple[Interval, Interval],
invalid_param: str,
invalid_value: Any,
):
domain_u, domain_v = sample_intervals
valid_params = {
"degreeU": 1,
"degreeV": 1,
"rational": True,
"pointData": [0.0, 0.0, 0.0, 1.0],
"countU": 1,
"countV": 1,
"knotsU": [0.0, 1.0],
"knotsV": [0.0, 1.0],
"domainU": domain_u,
"domainV": domain_v,
"closedU": False,
"closedV": False,
"units": Units.m,
}
valid_params[invalid_param] = invalid_value
with pytest.raises(SpeckleException):
Surface(**valid_params)
+21 -10
View File
@@ -24,24 +24,31 @@ class ServerTransport(AbstractTransport):
```py
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.transports.server import ServerTransport
# here's the data you want to send
block = Block(length=2, height=4)
# next create the server transport - this is the vehicle through which
# here's the project and model you want to send to
project_id = "abcdefghi"
model_id = "ihgfedcba"
# next, create the server transport - this is the vehicle through which
# you will send and receive
transport = ServerTransport(stream_id=new_stream_id, client=client)
transport = ServerTransport(stream_id=project_id, client=client)
# this serialises the block and sends it to the transport
hash = operations.send(base=block, transports=[transport])
# you can now create a commit on your stream with this object
commit_id = client.commit.create(
stream_id=new_stream_id,
obj_id=hash,
message="this is a block I made in speckle-py",
# you can now create tag this version of the model with this object
input = CreateVersionInput(
objectId = hash,
modelId = model_id,
projectId = project_id,
)
version = client.version.create(input)
```
"""
@@ -87,6 +94,12 @@ class ServerTransport(AbstractTransport):
self.session = requests.Session()
self.session.headers.update(
{
"Accept": "text/plain",
}
)
if self.account is not None:
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
@@ -94,7 +107,6 @@ class ServerTransport(AbstractTransport):
self.session.headers.update(
{
"Authorization": f"Bearer {self.account.token}",
"Accept": "text/plain",
}
)
@@ -127,8 +139,7 @@ class ServerTransport(AbstractTransport):
raise SpeckleException(
"Getting a single object using `ServerTransport.get_object()` is not"
" implemented. To get an object from the server, please use the"
" `SpeckleClient.object.get()` route",
" implemented.",
NotImplementedError(),
)
+6 -4
View File
@@ -6,7 +6,7 @@ from specklepy.objects.base import Base
@pytest.fixture(scope="session")
def base():
def base() -> Base:
base = Base()
base.name = "my_base"
base.units = "millimetres"
@@ -15,7 +15,9 @@ def base():
base.tuple = (1, 2, "3")
base.set = {1, 2, "3"}
base.vertices = [random.uniform(0, 10) for _ in range(1, 120)]
base.test_bases = [Base(name=i) for i in range(1, 22)]
base["@detach"] = Base(name="detached base")
base["@revit_thing"] = Base.of_type("SpecialRevitFamily", name="secret tho")
base.test_bases = [Base(applicationId=str(i)) for i in range(1, 22)]
base["@detach"] = Base(applicationId="detached base")
base["@revit_thing"] = Base.of_type(
"SpecialRevitFamily", applicationId="secret tho"
)
return base

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