Compare commits

..

89 Commits

Author SHA1 Message Date
Gergő Jedlicska 88b17db901 re-lock 2025-02-07 09:09:55 +01:00
Jedd Morgan 405972f681 Added newer DefaultTraversal rules to align with V3 sharp connectors (#367)
* First Pass

* Updated traversal

---------

Co-authored-by: KatKatKateryna <89912278+KatKatKateryna@users.noreply.github.com>
2025-02-03 10:41:13 +00:00
KatKatKateryna 0fbfff54d4 adding data object and QgisObjects and interface (#372)
* adding data object and QgisObjects and interface

* new classes

* python 3.9 typing

* rename file
2025-01-25 00:08:48 +08:00
Jedd Morgan 826dadc8c8 Merge pull request #378 from specklesystems/main
Main -> v3Dev
2025-01-24 14:53:33 +00:00
Dogukan Karatas b9e4ee2b23 Merge pull request #375 from specklesystems/dogukan/test_migration
fix: migration of existing tests
2025-01-24 15:34:54 +01:00
Chuck Driesler 78c55b787f chore(automate): improve error message when automate fails to receive a model version (#376)
Co-authored-by: Björn Steinhagen <steinhagen.bjoern@gmail.com>
2025-01-24 11:28:15 +00:00
Jedd Morgan 34f2dc2ab6 author now optional (#377) 2025-01-24 11:01:09 +00:00
Jedd Morgan a658e12cda Merge branch 'v3-dev' into dogukan/test_migration 2025-01-23 16:04:32 +00:00
Dogukan Karatas 85aa938fc2 Merge pull request #373 from specklesystems/dogukan/cnx-1006-add-missing-geometry-objects
feat: geometry objects with new structure
2025-01-23 17:01:37 +01:00
Dogukan Karatas 010fb83ea6 updated tests 2025-01-23 16:32:59 +01:00
Dogukan Karatas 7a291ce2f6 migrates tests 2025-01-23 15:51:34 +01:00
Dogukan Karatas 989c975c86 updated objects 2025-01-23 15:49:00 +01:00
Dogukan Karatas 516eff4d8b helper classes added 2025-01-23 14:30:09 +01:00
Dogukan Karatas 0650210601 removed re-export the objects 2025-01-22 14:11:51 +01:00
Dogukan Karatas b0b8140363 formatted 2025-01-22 13:17:19 +01:00
Dogukan Karatas d25f30b20d Merge branch 'v3-dev' into dogukan/cnx-1006-add-missing-geometry-objects 2025-01-22 11:43:13 +01:00
Dogukan Karatas b4e2f37b7f Merge pull request #374 from specklesystems/gergo/uvSetup
gergo/uvSetup
2025-01-22 10:49:17 +01:00
Dogukan Karatas b7ba2196f3 update uv.lock 2025-01-21 19:41:57 +01:00
Gergő Jedlicska 17cbcc38ba chore: just an echo of circleci 2025-01-21 12:58:51 +01:00
Gergő Jedlicska 9afb2c5c1c chore: add back and empty circleci config 2025-01-21 12:56:44 +01:00
Gergő Jedlicska eb13c9bc70 chore: remove stuff to please the ci gods 2025-01-21 12:50:58 +01:00
Dogukan Karatas a33588f3af makes it minimal 2025-01-21 12:48:52 +01:00
Gergő Jedlicska 970cf62e50 feat: add codecov upload 2025-01-20 21:36:12 +01:00
Gergő Jedlicska 513594c49f feat: run tests 2025-01-20 21:27:13 +01:00
Gergő Jedlicska 37c8e6dfb1 ci: run server in ci 2025-01-20 21:24:38 +01:00
Gergő Jedlicska 3859a88c4b chore: uv-lock use editable local package 2025-01-20 21:15:57 +01:00
Gergő Jedlicska dfa8fc99d9 docs: change readme to reference uv 2025-01-20 21:15:35 +01:00
Gergő Jedlicska ee97f3b718 chore: cleanup 2025-01-19 21:30:22 +01:00
Gergő Jedlicska e0b48f6123 feat: use dynamic version magic 2025-01-19 21:12:22 +01:00
Gergő Jedlicska 6fb6418d16 fix: checkout, duh... 2025-01-19 20:59:52 +01:00
Gergő Jedlicska ce104adb50 feat: add publish workflow 2025-01-19 20:58:26 +01:00
Gergő Jedlicska fe0a8eb9f5 chore: do not run on pr-s just yet 2025-01-19 20:28:39 +01:00
Gergő Jedlicska 6279dd3885 chore: update pre-commit repo to new version 2025-01-19 20:26:45 +01:00
Gergő Jedlicska 811c5843a9 chore: fix ruff import formatting 2025-01-19 20:23:21 +01:00
Gergő Jedlicska 035cd089e2 fix: missing ruff dev dependency added to project 2025-01-19 20:18:52 +01:00
Gergő Jedlicska 6daef049bb feat: run pre-commit in CI 2025-01-19 20:17:02 +01:00
Gergő Jedlicska d526c8ce3e feat: add github actions workflow 2025-01-19 20:09:06 +01:00
Gergő Jedlicska 4c91032718 chore: test fix WIP 2025-01-19 16:20:31 +01:00
Gergő Jedlicska ffb80457bc chore: fix all ruff issues 2025-01-19 16:13:21 +01:00
Gergő Jedlicska d380e6eaf8 chore: run ruff format 2025-01-19 15:18:40 +01:00
Gergő Jedlicska ace7c390c1 chore: run ruff format 2025-01-19 15:15:13 +01:00
Gergő Jedlicska c052dfad46 chore: run ruff format 2025-01-19 15:12:30 +01:00
Gergő Jedlicska 66802726b9 chore: run ruff format 2025-01-19 15:11:58 +01:00
Gergő Jedlicska b8f4150fb7 chore: set up uv as a project and package manager 2025-01-19 14:41:37 +01:00
Dogukan Karatas 255133010f updated mesh 2025-01-16 10:12:18 +01:00
Dogukan Karatas aea9bb3e1d caches precalculated values 2025-01-15 16:42:25 +01:00
Dogukan Karatas 5ca5334730 adds vertices check 2025-01-15 16:34:20 +01:00
Dogukan Karatas ba5f40a749 added area and volume calculation for meshes 2025-01-15 16:28:39 +01:00
Dogukan Karatas 04fc0fa715 to_list and from_list updated 2025-01-15 15:40:32 +01:00
Dogukan Karatas 2e80646d2c splitting tests 2025-01-14 14:38:48 +01:00
Dogukan Karatas fe6c18e97b adds docstrings 2025-01-14 10:28:59 +01:00
Dogukan Karatas 7c9058172f adds unit tests 2025-01-14 10:25:23 +01:00
Dogukan Karatas a82187589f added tests 2025-01-13 16:52:35 +01:00
Dogukan Karatas d811b010ff added missing geometries 2025-01-13 15:01:15 +01:00
Dogukan Karatas e1e5d9dbb6 feat(v-3): QGIS essentials (#369)
* adds qgis essentials

* moves intances under proxies

* remove defaults

* formatting
2025-01-07 17:38:06 +08:00
Gergő Jedlicska b17423b282 Merge pull request #366 from specklesystems/dogu/v3_objects
Object model v3 poc
2024-12-12 16:45:52 +01:00
Gergő Jedlicska 166b0f5e87 Merge branch 'v3-dev' of github.com:specklesystems/specklepy into dogu/v3_objects 2024-12-12 16:44:19 +01:00
Gergő Jedlicska cac34120a9 feat(v3_objects): migrate to full dataclass base objects 2024-12-12 16:41:31 +01:00
Gergő Jedlicska 55c4c68cf3 feat(base): use dataclass for base too 2024-12-12 16:34:42 +01:00
Dogukan Karatas be850d5ea9 updates interface 2024-12-12 15:25:09 +01:00
Dogukan Karatas c9a5badac1 implements IHasUnits 2024-12-11 14:55:00 +01:00
Gergő Jedlicska 118fa07e37 Merge branch 'main' of github.com:specklesystems/specklepy into v3-dev 2024-12-11 14:23:53 +01:00
Gergő Jedlicska d71b616e2b Merge pull request #363 from specklesystems/jrm/filter-tests
Added tests for filters
2024-12-11 14:21:54 +01:00
Jedd Morgan 35750f12c5 Merge branch 'main' into jrm/filter-tests 2024-12-11 13:05:18 +00:00
Dogukan Karatas 5730cdcb43 updates geometry classes 2024-12-11 14:04:24 +01:00
Jedd Morgan 82b6dbbe78 isort 2024-12-11 12:09:56 +00:00
Jedd Morgan 883be4b27b reexport inputs 2024-12-11 12:07:37 +00:00
Dogukan Karatas 37e2711a76 adds new object poc 2024-12-10 16:46:05 +01:00
Gergő Jedlicska 8dcc67fb31 Merge pull request #365 from specklesystems/jedd/cxpla-132-update-connectors-usage-of-user-queries-to-use-activeuser
updated active user streams
2024-12-10 15:57:04 +01:00
Jedd Morgan ed84820995 fix 2024-12-10 14:27:19 +00:00
Jedd Morgan 5c3dcb7bc0 updated active user streams 2024-12-10 14:14:31 +00:00
Gergő Jedlicska 92732e3c76 Merge pull request #364 from specklesystems/gergo/objectV3
gergo/objectV3
2024-12-10 15:04:19 +01:00
Gergő Jedlicska 903951547d chore(launchConfig): remove pytests args 2024-12-10 15:01:02 +01:00
Gergő Jedlicska 82c3dc9ffb feat(objects): collections and more 2024-12-09 14:29:01 +01:00
Gergő Jedlicska a0e10aae99 Merge pull request #362 from specklesystems/jrm/deps
Updated dependencies
2024-12-09 12:26:56 +01:00
Jedd Morgan bbea2a0d76 Aligned pre-commit-hooks 2024-12-09 11:24:38 +00:00
Jedd Morgan a05ac3479b Black reformat 2024-12-09 11:17:26 +00:00
Jedd Morgan 0bd972945e update dependencies 2024-12-09 11:14:54 +00:00
Jedd Morgan f200544065 Moved UserProjectsFilter to UserInputs 2024-12-09 10:58:31 +00:00
Jedd Morgan 68ce9823ae added filter tests 2024-12-09 10:56:17 +00:00
Mucahit Bilal GOKER a920352407 fix(api): rename 'onlyWithRole' to 'onlyWithRoles' in UserProjectsFilter (#361) 2024-12-06 15:53:10 +00:00
Dogukan Karatas 24bfb6718e adds performance tests 2024-12-06 14:25:07 +01:00
Gergő Jedlicska e63f4b8636 wip rework serialization for new object model 2024-12-06 10:53:05 +01:00
Gergő Jedlicska 47c6bd89af wip re-base 2024-12-06 09:46:05 +01:00
Chuck Driesler bd38dfacc7 fix(automate): include project id in run reporting (#356) 2024-11-26 14:48:43 +00:00
Chuck Driesler 281483f0fc fix(automate): add success result case (#355) 2024-11-21 12:07:48 +00:00
Jedd Morgan 932838de8f Revert collection speckle_type change (#354)
* revert collection speckle_type change

* black
2024-11-18 10:57:52 +00:00
Gergő Jedlicska a0b39e4c64 Merge pull request #353 from specklesystems/jrm/api-fix
Updated version mutation inputs
2024-11-13 12:49:49 +01:00
Jedd Morgan 759cd0ef58 Updated version mutation inputs 2024-11-13 10:46:59 +00:00
113 changed files with 3660 additions and 5916 deletions
+7 -100
View File
@@ -1,108 +1,15 @@
version: 2.1
orbs:
codecov: codecov/codecov@3.3.0
# Define the jobs we want to run for this project
jobs:
pre-commit:
parameters:
config_file:
default: ./.pre-commit-config.yaml
description: Optional, path to pre-commit config file.
type: string
cache_prefix:
default: ''
description: |
Optional cache prefix to be used on CircleCI. Can be used for cache busting or to ensure multiple jobs use different caches.
type: string
build:
docker:
- image: speckle/pre-commit-runner:latest
resource_class: medium
- image: cimg/base:2023.03
steps:
- checkout
- restore_cache:
keys:
- cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
- run:
name: Install pre-commit hooks
command: pre-commit install-hooks --config <<parameters.config_file>>
- save_cache:
key: cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
paths:
- ~/.cache/pre-commit
- run:
name: Run pre-commit
command: pre-commit run --all-files
- run:
command: git --no-pager diff
name: git diff
when: on_fail
test:
machine:
image: ubuntu-2204:2023.02.1
docker_layer_caching: false
resource_class: medium
parameters:
tag:
default: "3.11"
type: string
steps:
- checkout
- run:
name: Install python
command: |
pyenv install -s << parameters.tag >>
pyenv global << parameters.tag >>
- run:
name: Startup the Speckle Server
command: docker compose -f docker-compose.yml up -d
- run:
name: Install Poetry
command: |
pip install poetry
- run:
name: Install packages
command: poetry install
- run:
name: Run tests
command: poetry run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
- store_test_results:
path: reports
- store_artifacts:
path: reports
- codecov/upload
deploy:
docker:
- image: "cimg/python:3.8"
steps:
- checkout
- run: python patch_version.py $CIRCLE_TAG
- run: poetry build
- run: poetry publish -u __token__ -p $PYPI_TOKEN
- run: echo "so long and thanks for all the fish"
# Orchestrate our job run sequence
workflows:
main:
build_and_test:
jobs:
- pre-commit:
filters:
tags:
only: /.*/
- test:
matrix:
parameters:
tag: ["3.11"]
filters:
tags:
only: /.*/
- deploy:
context: pypi
requires:
- pre-commit
- test
filters:
tags:
only: /[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/ # For testing only! /ci\/.*/
- build
+56
View File
@@ -0,0 +1,56 @@
name: "Specklepy test and build"
on:
# pull_request:
# branches:
# - 'v3-dev'
push:
branches:
- "gergo/uvSetup"
jobs:
ci:
name: continuous-integration
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.10"
- "3.11"
- "3.12"
- "3.13"
steps:
- uses: actions/checkout@v4
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v5
with:
python-version: ${{ matrix.python-version }}
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install the project
run: uv sync --all-extras --dev
- uses: actions/cache@v3
with:
path: ~/.cache/pre-commit/
key: ${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit
run: uv run pre-commit run --all-files
# - name: Run Speckle Server
# run: docker compose up -d
# - name: Run tests
# run: uv run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
# - uses: codecov/codecov-action@v5
# if: matrix.python-version == 3.13
# with:
# fail_ci_if_error: true # optional (default = false)
# files: ./reports/test-results.xml # optional
# token: ${{ secrets.CODECOV_TOKEN }}
- name: Minimize uv cache
run: uv cache prune --ci
+36
View File
@@ -0,0 +1,36 @@
# Publish a release to PyPI.
# name: 'Publish to PyPI'
# on:
# push:
# branches:
# - 'gergo/uvSetup'
# jobs:
# pypi-publish:
# name: Upload to PyPI
# runs-on: ubuntu-latest
# environment:
# name: release
# permissions:
# # For PyPI's trusted publishing.
# id-token: write
# steps:
# - name: 'Install uv'
# uses: astral-sh/setup-uv@v5
# - uses: actions/checkout@v4
# with:
# # This is necessary so that we have the tags.
# fetch-depth: 0
# - uses: mtkennerly/dunamai-action@v1
# with:
# env-var: MY_VERSION
# args: --style semver
# - run: echo $MY_VERSION
# - name: 'Build artifacts'
# run: uv build
# - name: Publish to PyPi
# run: uv publish --publish-url https://test.pypi.org/simple/
# - name: Test package install
# run: uv run --with specklepy --no-project -- python -c "import specklepy"
+15 -17
View File
@@ -1,33 +1,31 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
- repo: local
hooks:
# Run the linter.
- id: ruff
rev: v0.1.6
name: ruff lint
entry: uv run ruff check --force-exclude
language: system
types_or: [python, pyi]
# Run the formatter.
- id: ruff-format
name: ruff format
entry: uv run ruff format --force-exclude
language: system
types_or: [python, pyi]
- repo: https://github.com/commitizen-tools/commitizen
hooks:
- id: commitizen
- id: commitizen-branch
stages:
- push
- pre-push
rev: v3.13.0
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/psf/black
rev: 23.11.0
hooks:
- id: black
# It is recommended to specify the latest version of Python
# supported by your project here, or alternatively use
# pre-commit's default_language_version, see
# https://pre-commit.com/#top_level-default_language_version
# language_version: python3.11
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+3 -5
View File
@@ -4,11 +4,9 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
@@ -16,9 +14,9 @@
},
{
"name": "Pytest",
"type": "python",
"type": "debugpy",
"request": "launch",
"program": "pytest",
"module": "pytest",
"args": [],
"console": "integratedTerminal",
"justMyCode": true
+7 -7
View File
@@ -25,25 +25,25 @@ Head to the [**📚 specklepy docs**](https://speckle.guide/dev/python.html) for
### Installation
This project uses python-poetry for dependency management, make sure you follow the official [docs](https://python-poetry.org/docs/#installation) to get poetry.
This project uses uv for dependency management, make sure you follow the official [docs](https://docs.astral.sh/uv/) to get it.
To bootstrap the project environment run `$ poetry install`. This will create a new virtual-env for the project and install both the package and dev dependencies.
To create a new virtual environment with uv run `$ uv venv` and follow the instructions on the screen to activate the virtual environment.
To bootstrap the project environment run `$ uv sync`. This will install both the package and dev dependencies.
If this is your first time using poetry and you're used to creating your venvs within the project directory, run `poetry config virtualenvs.in-project true` to configure poetry to do the same.
To execute any python script run `$ uv run python my_script.py`
To execute any python script run `$ poetry run python my_script.py`
> Alternatively you may roll your own virtual-env with either venv, virtualenv, pyenv-virtualenv etc. Poetry will play along an recognize if it is invoked from inside a virtual environment.
> Alternatively you may roll your own virtual-env with either venv, virtualenv, pyenv-virtualenv etc. Uv will play along an recognize if it is invoked from inside a virtual environment.
### Style guide
All our repo wide styling linting and other rules are checked and enforced by `pre-commit`, which is included in the dev dependencies.
All our repo wide styling linting and other rules are checked and enforced by `pre-commit`, which is included in the dev dependencies.
It is recommended to set up `pre-commit` after installing the dependencies by running `$ pre-commit install`.
Commiting code that doesn't adhere to the given rules, will fail the checks in our CI system.
### Local Data Paths
It may be helpful to know where the local accounts and object cache dbs are stored. Depending on on your OS, you can find the dbs at:
- Windows: `APPDATA` or `<USER>\AppData\Roaming\Speckle`
- Linux: `$XDG_DATA_HOME` or by default `~/.local/share/Speckle`
- Mac: `~/.config/Speckle`
+1 -7
View File
@@ -6,7 +6,7 @@ services:
# Speckle Server dependencies
#######
postgres:
image: "postgres:14.5-alpine"
image: "postgres:16-alpine"
restart: always
environment:
POSTGRES_DB: speckle
@@ -53,12 +53,6 @@ services:
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend-2:latest
restart: always
ports:
- "0.0.0.0:8080:8080"
speckle-server:
image: speckle/speckle-server:latest
restart: always
+2 -2
View File
@@ -1,8 +1,8 @@
from devtools import debug
from specklepy.api import operations
from specklepy.objects.geometry import Base
from specklepy.objects.units import Units
from specklepy.objects_v2.geometry import Base
from specklepy.objects_v2.units import Units
dct = {
"id": "1234abcd",
-31
View File
@@ -1,31 +0,0 @@
import re
import sys
def patch(tag):
print(f"Patching version: {tag}")
with open("pyproject.toml", "r") as f:
lines = f.readlines()
if "version" not in lines[2]:
raise Exception("Invalid pyproject.toml. Could not patch version.")
lines[2] = f'version = "{tag}"\n'
with open("pyproject.toml", "w") as file:
file.writelines(lines)
def main():
if len(sys.argv) < 2:
return
tag = sys.argv[1]
if not re.match(r"[0-9]+(\.[0-9]+)*$", tag):
raise ValueError(f"Invalid tag provided: {tag}")
patch(tag)
if __name__ == "__main__":
main()
Generated
-2040
View File
File diff suppressed because it is too large Load Diff
+56 -61
View File
@@ -1,75 +1,70 @@
[tool.poetry]
[project]
dynamic = ["version"]
name = "specklepy"
version = "2.17.14"
description = "The Python SDK for Speckle 2.0"
readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"]
license = "Apache-2.0"
repository = "https://github.com/specklesystems/speckle-py"
documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
packages = [
{ include = "specklepy", from = "src" },
{ include = "speckle_automate", from = "src" },
authors = [{ name = "Speckle Systems", email = "devops@speckle.systems" }]
license = { text = "Apache-2.0" }
requires-python = ">=3.10.0, <4.0"
dependencies = [
"appdirs>=1.4.4",
"attrs>=24.3.0",
"deprecated>=1.2.15",
"gql[requests,websockets]>=3.5.0",
"httpx>=0.28.1",
"pydantic>=2.10.5",
"pydantic-settings>=2.7.1",
"stringcase>=1.2.0",
"ujson>=5.10.0",
]
[dependency-groups]
dev = [
"commitizen>=4.1.0",
"devtools>=0.12.2",
"pre-commit>=4.0.1",
"pytest>=8.3.4",
"pytest-asyncio>=0.25.2",
"pytest-cov>=6.0.0",
"pytest-ordering>=0.6",
"ruff>=0.9.2",
"types-deprecated>=1.2.15.20241117",
"types-requests>=2.32.0.20241016",
"types-ujson>=5.10.0.20240515",
]
[tool.poetry.dependencies]
python = ">=3.8.0, <4.0"
pydantic = "^2.5"
appdirs = "^1.4.4"
gql = { extras = ["requests", "websockets"], version = "^3.3.0" }
ujson = "^5.3.0"
Deprecated = "^1.2.13"
stringcase = "^1.2.0"
attrs = "^23.1.0"
httpx = "^0.25.0"
[project.urls]
repository = "https://github.com/specklesystems/specklepy"
documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/"
[tool.poetry.group.dev.dependencies]
black = "23.11.0"
isort = "^5.7.0"
pytest = "^7.1.3"
pytest-asyncio = "^0.23.0"
pytest-ordering = "^0.6"
pytest-cov = "^3.0.0"
devtools = "^0.8.0"
pylint = "^2.14.4"
pydantic-settings = "^2.3.0"
mypy = "^0.982"
pre-commit = "^2.20.0"
commitizen = "^2.38.0"
ruff = "^0.4.4"
types-deprecated = "^1.2.9"
types-ujson = "^5.6.0.0"
types-requests = "^2.28.11.5"
[tool.black]
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
include = '\.pyi?$'
line-length = 88
target-version = ["py37", "py38", "py39", "py310", "py311"]
[build-system]
requires = ["setuptools>=64", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
[tool.commitizen]
name = "cz_conventional_commits"
version = "2.9.2"
tag_format = "$version"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.isort]
profile = "black"
[tool.ruff]
exclude = [".venv", "**/*.yml"]
[tool.ruff.lint]
select = [
# pycodestyle
"E",
# Pyflakes
"F",
# pyupgrade
"UP",
# flake8-bugbear
"B",
# flake8-simplify
"SIM",
# isort
"I",
]
ignore = ["UP006", "UP007", "UP035"]
+36 -4
View File
@@ -96,13 +96,23 @@ class AutomationContext:
def receive_version(self) -> Base:
"""Receive the Speckle project version that triggered this automation run."""
# TODO: this is a quick hack to keep implementation consistency. Move to proper receive many versions
# TODO: this is a quick hack to keep implementation consistency.
# Move to proper receive many versions
version_id = self.automation_run_data.triggers[0].payload.version_id
commit = self.speckle_client.commit.get(
self.automation_run_data.project_id, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
if not commit or not commit.referencedObject:
raise ValueError(
f"""\
Could not receive specified version.
{"The commit has no referencedObject." if not commit.referencedObject else ""}
Is your environment configured correctly?
project_id: {self.automation_run_data.project_id}
model_id: {self.automation_run_data.triggers[0].payload.model_id}
version_id: {self.automation_run_data.triggers[0].payload.version_id}
"""
)
base = operations.receive(
commit.referencedObject, self._server_transport, self._memory_transport
)
@@ -206,6 +216,7 @@ class AutomationContext:
query = gql(
"""
mutation AutomateFunctionRunStatusReport(
$projectId: String!
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
@@ -213,6 +224,7 @@ class AutomationContext:
$contextView: String
){
automateFunctionRunStatusReport(input: {
projectId: $projectId
functionRunId: $functionRunId
status: $status
statusMessage: $statusMessage
@@ -236,6 +248,7 @@ class AutomationContext:
object_results = None
params = {
"projectId": self.automation_run_data.project_id,
"functionRunId": self.automation_run_data.function_run_id,
"status": self.run_status.value,
"statusMessage": self._automation_result.status_message,
@@ -261,7 +274,8 @@ class AutomationContext:
if not path_obj.exists():
raise ValueError("The given file path doesn't exist")
files = {path_obj.name: open(str(path_obj), "rb")}
files = {path_obj.name: path_obj.open("rb")}
url = (
f"{self.automation_run_data.speckle_server_url}api/stream/"
@@ -355,6 +369,24 @@ class AutomationContext:
visual_overrides,
)
def attach_success_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new success case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.SUCCESS,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_info_to_objects(
self,
category: str,
+1
View File
@@ -1,4 +1,5 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
+7 -10
View File
@@ -61,15 +61,13 @@ def _parse_input_data(
def execute_automate_function(
automate_function: AutomateFunction[T],
input_schema: type[T],
) -> None:
...
) -> None: ...
@overload
def execute_automate_function(
automate_function: AutomateFunctionWithoutInputs,
) -> None:
...
) -> None: ...
class AutomateGenerateJsonSchema(GenerateJsonSchema):
@@ -130,7 +128,8 @@ def execute_automate_function(
automate_function, # type: ignore
)
# if we've gotten this far, the execution should technically be completed as expected
# if we've gotten this far,
# the execution should technically be completed as expected
# thus exiting with 0 is the schemantically correct thing to do
exit_code = (
1 if automation_context.run_status == AutomationStatus.EXCEPTION else 0
@@ -146,16 +145,14 @@ def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunction[T],
inputs: T,
) -> AutomationContext:
...
) -> AutomationContext: ...
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunctionWithoutInputs,
) -> AutomationContext:
...
) -> AutomationContext: ...
def run_function(
@@ -194,4 +191,4 @@ def run_function(
if not automation_context.context_view:
automation_context.set_context_view()
automation_context.report_run_status()
return automation_context
return automation_context
+1
View File
@@ -69,6 +69,7 @@ class AutomationStatus(str, Enum):
class ObjectResultLevel(str, Enum):
"""Possible status message levels for object reports."""
SUCCESS = "SUCCESS"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
+2 -2
View File
@@ -1,3 +1,3 @@
from specklepy import objects
# from specklepy import objects
__all__ = ["objects"]
# __all__ = ["objects"]
+8 -6
View File
@@ -1,3 +1,5 @@
import contextlib
from deprecated import deprecated
from specklepy.api.credentials import Account
@@ -7,12 +9,12 @@ from specklepy.api.resources import (
OtherUserResource,
ProjectInviteResource,
ProjectResource,
ServerResource,
SubscriptionResource,
VersionResource,
branch,
commit,
object,
server,
stream,
subscriptions,
user,
@@ -40,7 +42,8 @@ class SpeckleClient(CoreSpeckleClient):
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
# authenticate the client with an account
# (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate_with_account(account)
@@ -69,15 +72,14 @@ class SpeckleClient(CoreSpeckleClient):
self.account = Account()
def _init_resources(self) -> None:
self.server = server.Resource(
self.server = ServerResource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
try:
with contextlib.suppress(Exception):
server_version = self.server.version()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
+5 -2
View File
@@ -2,8 +2,11 @@ from typing import List, Optional
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.credentials import StreamWrapper # noqa: F401
from specklepy.core.api.credentials import Account, UserInfo # noqa: F401
from specklepy.core.api.credentials import ( # noqa: F401
Account,
StreamWrapper, # noqa: F401
UserInfo,
)
from specklepy.core.api.credentials import (
get_account_from_token as core_get_account_from_token,
)
+5 -1
View File
@@ -53,7 +53,9 @@ def receive(
return _untracked_receive(obj_id, remote_transport, local_transport)
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
def serialize(
base: Base, write_transports: List[AbstractTransport] | None = None
) -> str:
"""
Serialize a base object. If no write transports are provided,
the object will be serialized
@@ -67,6 +69,8 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
Returns:
str -- the serialized object
"""
if not write_transports:
write_transports = []
metrics.track(metrics.SDK, custom_props={"name": "Serialize"})
return core_serialize(base, write_transports)
@@ -3,8 +3,7 @@ from typing import List, Optional, overload
from deprecated import deprecated
from specklepy.core.api.inputs.project_inputs import UserProjectsFilter
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.models import (
PendingStreamCollaborator,
Project,
@@ -44,12 +43,10 @@ class ActiveUserResource(CoreResource):
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
) -> User:
...
) -> User: ...
@overload
def update(self, *, input: UserUpdateInput) -> User:
...
def update(self, *, input: UserUpdateInput) -> User: ...
def update(
self,
@@ -141,7 +138,8 @@ class ActiveUserResource(CoreResource):
token (Optional[str]): The token of the invite to look for (optional).
Returns:
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
Optional[PendingStreamCollaborator]: The invite for the given stream,
or None if not found.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
@@ -20,8 +20,10 @@ from specklepy.logging.exceptions import SpeckleException
class OtherUserResource(CoreResource):
"""
Provides API access to other users' profiles and activities on the platform.
This class enables fetching limited information about users, searching for users by name or email,
and accessing user activity logs with appropriate privacy and access control measures in place.
This class enables fetching limited information about users,
searching for users by name or email,
and accessing user activity logs with appropriate privacy
and access control measures in place.
"""
def __init__(self, account, basepath, client, server_version) -> None:
@@ -64,7 +66,8 @@ class OtherUserResource(CoreResource):
limit (int): Maximum number of search results to return.
Returns:
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
Union[List[LimitedUser], SpeckleException]:
A list of users matching the search
query or an exception if the query is too short.
"""
if len(search_query) < 3:
@@ -86,8 +89,8 @@ class OtherUserResource(CoreResource):
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
Retrieves a collection of activities for a specified user,
with optional filters for activity type, time frame, and pagination.
Args:
user_id (str): The ID of the user whose activities are being requested.
@@ -98,7 +101,8 @@ class OtherUserResource(CoreResource):
cursor (Optional[datetime]): Timestamp for pagination cursor.
Returns:
ActivityCollection: A collection of user activities filtered according to specified criteria.
ActivityCollection: A collection of user activities filtered
according to specified criteria.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@@ -31,7 +31,8 @@ class ServerResource(CoreResource):
the server version in the format (major, minor, patch, (tag, build))
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
"""
# not tracking as it will be called along with other mutations / queries as a check
# not tracking as it will be called along with other
# mutations / queries as a check
return super().version()
def apps(self) -> Dict:
@@ -4,6 +4,6 @@ from specklepy.api.resources import ServerResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION)
@deprecated(reason="Renamed to ServerResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""Renamed to ServerResource"""
+12 -10
View File
@@ -1,3 +1,4 @@
import contextlib
import re
from typing import Dict
from warnings import warn
@@ -49,7 +50,8 @@ class SpeckleClient:
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
# authenticate the client with an account
# (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate_with_account(account)
@@ -102,7 +104,8 @@ class SpeckleClient:
self._init_resources()
# ? Check compatibility with the server - i think we can skip this at this point? save a request
# ? Check compatibility with the server
# - i think we can skip this at this point? save a request
# try:
# server_info = self.server.get()
# if isinstance(server_info, Exception):
@@ -187,9 +190,10 @@ class SpeckleClient:
if ex.exception.code == 403:
warn(
SpeckleWarning(
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
)
"Possibly invalid token - could not authenticate "
f"Speckle Client for server {self.url}"
),
stacklevel=2,
)
else:
raise ex
@@ -203,10 +207,8 @@ class SpeckleClient:
)
server_version = None
try:
with contextlib.suppress(Exception):
server_version = self.server.version()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
@@ -283,7 +285,7 @@ class SpeckleClient:
return attr.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
except AttributeError:
except AttributeError as ex:
raise SpeckleException(
f"Method {name} is not supported by the SpeckleClient class"
)
) from ex
+42
View File
@@ -0,0 +1,42 @@
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
ModelVersionsFilter,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectInviteCreateInput,
ProjectInviteUseInput,
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.inputs.version_inputs import (
CreateVersionInput,
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
__all__ = [
"CreateModelInput",
"DeleteModelInput",
"UpdateModelInput",
"ModelVersionsFilter",
"ProjectCreateInput",
"ProjectInviteCreateInput",
"ProjectInviteUseInput",
"ProjectModelsFilter",
"ProjectUpdateInput",
"ProjectUpdateRoleInput",
"UserProjectsFilter",
"UserUpdateInput",
"UpdateVersionInput",
"MoveVersionsInput",
"DeleteVersionsInput",
"CreateVersionInput",
"MarkReceivedVersionInput",
]
@@ -45,8 +45,3 @@ class ProjectUpdateRoleInput(BaseModel):
userId: str
projectId: str
role: Optional[str]
class UserProjectsFilter(BaseModel):
search: str
onlyWithRole: Optional[Sequence[str]] = None
+6 -1
View File
@@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, Sequence
from pydantic import BaseModel
@@ -8,3 +8,8 @@ class UserUpdateInput(BaseModel):
bio: Optional[str] = None
company: Optional[str] = None
name: Optional[str] = None
class UserProjectsFilter(BaseModel):
search: str
onlyWithRoles: Optional[Sequence[str]] = None
@@ -5,16 +5,19 @@ from pydantic import BaseModel
class UpdateVersionInput(BaseModel):
versionId: str
projectId: str
message: Optional[str]
class MoveVersionsInput(BaseModel):
targetModelName: str
versionIds: Sequence[str]
projectId: str
class DeleteVersionsInput(BaseModel):
versionIds: Sequence[str]
projectId: str
class CreateVersionInput(BaseModel):
+3 -2
View File
@@ -55,7 +55,8 @@ class ServerConfiguration(BaseModel):
objectSizeLimitBytes: int
# Keeping this one all Optionals at the minute, because its used both as a deserialization model for GQL and Account Management
# Keeping this one all Optionals at the minute,
# because its used both as a deserialization model for GQL and Account Management
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
@@ -126,7 +127,7 @@ class Version(BaseModel):
class Model(BaseModel):
author: LimitedUser
author: Optional[LimitedUser]
createdAt: datetime
description: Optional[str]
displayName: str
+4 -1
View File
@@ -4,7 +4,10 @@ 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_REASON = (
"Stream/Branch/Commit API is now deprecated, "
"Use the new Project/Model/Version API functions in Client"
)
FE1_DEPRECATION_VERSION = "2.20"
+7 -2
View File
@@ -70,7 +70,8 @@ def receive(
serializer = BaseObjectSerializer(read_transport=local_transport)
# try local transport first. if the parent is there, we assume all the children are there and continue with deserialization using the local transport
# try local transport first. if the parent is there, we assume all the children
# are there and continue with deserialization using the local transport
obj_string = local_transport.get_object(obj_id)
if obj_string:
return serializer.read_json(obj_string=obj_string)
@@ -90,7 +91,9 @@ def receive(
return serializer.read_json(obj_string=obj_string)
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
def serialize(
base: Base, write_transports: List[AbstractTransport] | None = None
) -> str:
"""
Serialize a base object. If no write transports are provided,
the object will be serialized
@@ -104,6 +107,8 @@ def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str
Returns:
str -- the serialized object
"""
if not write_transports:
write_transports = []
serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1]
+3 -2
View File
@@ -18,7 +18,7 @@ from specklepy.transports.sqlite import SQLiteTransport
T = TypeVar("T", bound=BaseModel)
class ResourceBase(object):
class ResourceBase:
def __init__(
self,
account: Account,
@@ -101,7 +101,8 @@ class ResourceBase(object):
parse_response: bool = True,
) -> Any:
"""Executes the GraphQL query"""
# This method has quite complex and ambiguous typing, and counter-intuitive error handling
# This method has quite complex and ambiguous typing,
# and counter-intuitive error handling
# We are going to phase it out in favour of `make_request_and_parse_response`
try:
with self.__lock:
@@ -4,8 +4,7 @@ from typing import List, Optional, overload
from deprecated import deprecated
from gql import gql
from specklepy.core.api.inputs.project_inputs import UserProjectsFilter
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
@@ -38,10 +37,12 @@ class ActiveUserResource(ResourceBase):
self.schema = User
def get(self) -> Optional[User]:
"""Gets the currently active user profile (as extracted from the authorization header)
"""Gets the currently active user profile
(as extracted from the authorization header)
Returns:
User -- the requested user, or none if no authentication token is provided to the Client
User -- the requested user, or none if no authentication token
is provided to the Client
"""
QUERY = gql(
"""
@@ -100,12 +101,10 @@ class ActiveUserResource(ResourceBase):
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
) -> User:
...
) -> User: ...
@overload
def update(self, *, input: UserUpdateInput) -> User:
...
def update(self, *, input: UserUpdateInput) -> User: ...
def update(
self,
@@ -76,14 +76,24 @@ class ModelResource(ResourceBase):
) -> ModelWithVersions:
QUERY = gql(
"""
query ModelGetWithVersions($modelId: String!, $projectId: String!, $versionsLimit: Int!, $versionsCursor: String, $versionsFilter: ModelVersionsFilter) {
query ModelGetWithVersions(
$modelId: String!,
$projectId: String!,
$versionsLimit: Int!,
$versionsCursor: String,
$versionsFilter: ModelVersionsFilter
) {
data:project(id: $projectId) {
data:model(id: $modelId) {
id
name
previewUrl
updatedAt
versions(limit: $versionsLimit, cursor: $versionsCursor, filter: $versionsFilter) {
versions(
limit: $versionsLimit,
cursor: $versionsCursor,
filter: $versionsFilter
) {
items {
id
referencedObject
@@ -127,9 +137,11 @@ class ModelResource(ResourceBase):
"modelId": model_id,
"versionsLimit": versions_limit,
"versionsCursor": versions_cursor,
"versionsFilter": versions_filter.model_dump(warnings="error")
if versions_filter
else None,
"versionsFilter": (
versions_filter.model_dump(warnings="error")
if versions_filter
else None
),
}
return self.make_request_and_parse_response(
@@ -146,9 +158,18 @@ class ModelResource(ResourceBase):
) -> ResourceCollection[Model]:
QUERY = gql(
"""
query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) {
query ProjectGetWithModels(
$projectId: String!,
$modelsLimit: Int!,
$modelsCursor: String,
$modelsFilter: ProjectModelsFilter
) {
data:project(id: $projectId) {
data:models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) {
data:models(
limit: $modelsLimit,
cursor: $modelsCursor,
filter: $modelsFilter
) {
items {
id
name
@@ -179,9 +200,9 @@ class ModelResource(ResourceBase):
"projectId": project_id,
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": models_filter.model_dump(warnings="error")
if models_filter
else None,
"modelsFilter": (
models_filter.model_dump(warnings="error") if models_filter else None
),
}
return self.make_request_and_parse_response(
@@ -74,7 +74,9 @@ class OtherUserResource(ResourceBase):
archived: bool = False,
emailOnly: bool = False,
) -> UserSearchResultCollection:
"""Searches for a user on the server, by name or email. The search query must be at least
"""
Searches for a user on the server, by name or email.
The search query must be at least
3 characters long
Arguments:
@@ -89,8 +91,20 @@ class OtherUserResource(ResourceBase):
QUERY = gql(
"""
query UserSearch($query: String!, $limit: Int!, $cursor: String, $archived: Boolean, $emailOnly: Boolean) {
data:userSearch(query: $query, limit: $limit, cursor: $cursor, archived: $archived, emailOnly: $emailOnly) {
query UserSearch(
$query: String!,
$limit: Int!,
$cursor: String,
$archived: Boolean,
$emailOnly: Boolean
) {
data:userSearch(
query: $query,
limit: $limit,
cursor: $cursor,
archived: $archived,
emailOnly: $emailOnly
) {
cursor
items {
id
@@ -37,7 +37,10 @@ class ProjectInviteResource(ResourceBase):
) -> ProjectWithTeam:
QUERY = gql(
"""
mutation ProjectInviteCreate($projectId: ID!, $input: ProjectInviteCreateInput!) {
mutation ProjectInviteCreate(
$projectId: ID!,
$input: ProjectInviteCreateInput!
) {
data:projectMutations {
data:invites {
data:create(projectId: $projectId, input: $input) {
@@ -65,7 +65,12 @@ class ProjectResource(ResourceBase):
) -> ProjectWithModels:
QUERY = gql(
"""
query ProjectGetWithModels($projectId: String!, $modelsLimit: Int!, $modelsCursor: String, $modelsFilter: ProjectModelsFilter) {
query ProjectGetWithModels(
$projectId: String!,
$modelsLimit: Int!,
$modelsCursor: String,
$modelsFilter: ProjectModelsFilter
) {
data:project(id: $projectId) {
id
name
@@ -77,7 +82,11 @@ class ProjectResource(ResourceBase):
updatedAt
sourceApps
workspaceId
models(limit: $modelsLimit, cursor: $modelsCursor, filter: $modelsFilter) {
models(
limit: $modelsLimit,
cursor: $modelsCursor,
filter: $modelsFilter
) {
items {
id
name
@@ -108,9 +117,9 @@ class ProjectResource(ResourceBase):
"projectId": project_id,
"modelsLimit": models_limit,
"modelsCursor": models_cursor,
"modelsFilter": models_filter.model_dump(warnings="error")
if models_filter
else None,
"modelsFilter": (
models_filter.model_dump(warnings="error") if models_filter else None
),
}
return self.make_request_and_parse_response(
@@ -80,7 +80,8 @@ class ServerResource(ResourceBase):
the server version in the format (major, minor, patch, (tag, build))
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
"""
# not tracking as it will be called along with other mutations / queries as a check
# not tracking as it will be called along with other mutations / queries
# as a check
query = gql(
"""
query Server {
@@ -76,7 +76,13 @@ class VersionResource(ResourceBase):
) -> ResourceCollection[Version]:
QUERY = gql(
"""
query VersionGetVersions($projectId: String!, $modelId: String!, $limit: Int!, $cursor: String, $filter: ModelVersionsFilter) {
query VersionGetVersions(
$projectId: String!,
$modelId: String!,
$limit: Int!,
$cursor: String,
$filter: ModelVersionsFilter
) {
data:project(id: $projectId) {
data:model(id: $modelId) {
data:versions(limit: $limit, cursor: $cursor, filter: $filter) {
@@ -111,7 +111,7 @@ class Resource(ResourceBase):
query = gql(
"""
query User($stream_limit: Int!) {
user {
activeUser {
id
bio
name
@@ -149,7 +149,7 @@ class Resource(ResourceBase):
params = {"stream_limit": stream_limit}
return self.make_request(
query=query, params=params, return_type=["user", "streams", "items"]
query=query, params=params, return_type=["activeUser", "streams", "items"]
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
+16 -5
View File
@@ -159,11 +159,12 @@ class StreamWrapper:
try:
self.branch_name = project["project"]["model"]["name"]
except KeyError as ke:
raise SpeckleException("Project model name is not found", ke)
raise SpeckleException("Project model name is not found", ke) from ke
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no {key_stream} id found."
f"Cannot parse {url} into a stream wrapper class - no {key_stream} ",
"id found.",
)
@property
@@ -213,7 +214,11 @@ class StreamWrapper:
self._client = SpeckleClient(host=self.host, use_ssl=self.use_ssl)
if self._account.token is None and token is None:
warn(f"No local account found for server {self.host}", SpeckleWarning)
warn(
f"No local account found for server {self.host}",
SpeckleWarning,
stacklevel=2,
)
return self._client
if self._account.token:
@@ -266,14 +271,20 @@ class StreamWrapper:
if use_fe2 is False or (use_fe2 is True and not self.model_id):
base_url = f"{self.server_url}{key_streams}{self.stream_id}"
else: # fe2 is True and model_id available
base_url = f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
base_url = (
f"{self.server_url}{key_streams}"
f"{self.stream_id}{key_branches}{value_branch}"
)
if wrapper_type == "object":
return f"{base_url}{key_objects}{self.object_id}"
elif wrapper_type == "commit":
return f"{base_url}{key_commits}{self.commit_id}"
elif wrapper_type == "branch":
return f"{self.server_url}{key_streams}{self.stream_id}{key_branches}{value_branch}"
return (
f"{self.server_url}{key_streams}{self.stream_id}"
f"{key_branches}{value_branch}"
)
elif wrapper_type == "stream":
return f"{self.server_url}{key_streams}{self.stream_id}"
else:
@@ -1,6 +1,7 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
@@ -98,7 +99,7 @@ def user_application_data_path() -> Path:
except Exception as ex:
raise SpeckleException(
message="Failed to initialize user application data path.", exception=ex
)
) from ex
def user_speckle_folder_path() -> Path:
+3 -2
View File
@@ -86,7 +86,8 @@ def track(
METRICS_TRACKER.queue.put_nowait(event_params)
except Exception as ex:
# wrapping this whole thing in a try except as we never want a failure here to annoy users!
# wrapping this whole thing in a try except as we never want a failure here
# to annoy users!
LOG.debug(f"Error queueing metrics request: {str(ex)}")
@@ -106,7 +107,7 @@ class Singleton(type):
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
-15
View File
@@ -1,15 +0,0 @@
from typing import Optional
from specklepy.objects.base import Base
class CRS(Base, speckle_type="Objects.GIS.CRS"):
"""A Coordinate Reference System stored in wkt format"""
name: Optional[str] = None
authority_id: Optional[str] = None
wkt: Optional[str] = None
units_native: Optional[str] = None
offset_x: Optional[float] = None
offset_y: Optional[float] = None
rotation: Optional[float] = None
-24
View File
@@ -1,24 +0,0 @@
"""Builtin Speckle object kit."""
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.GIS.geometry import (
GisLineElement,
GisPointElement,
GisPolygonElement,
GisPolygonGeometry,
GisRasterElement,
PolygonGeometry,
)
from specklepy.objects.GIS.layers import RasterLayer, VectorLayer
__all__ = [
"VectorLayer",
"RasterLayer",
"GisPolygonGeometry",
"PolygonGeometry",
"GisPolygonElement",
"GisLineElement",
"GisPointElement",
"GisRasterElement",
"CRS",
]
-74
View File
@@ -1,74 +0,0 @@
from typing import List, Optional, Union
from specklepy.objects.base import Base
from specklepy.objects.geometry import (
Arc,
Circle,
Line,
Mesh,
Point,
Polycurve,
Polyline,
)
class PolygonGeometry(Base, speckle_type="Objects.GIS.PolygonGeometry"):
"""GIS Polygon Geometry"""
boundary: Optional[Polyline]
voids: Optional[List[Polyline]]
GisPolygonGeometry = PolygonGeometry
class GisPolygonElement(Base, speckle_type="Objects.GIS.PolygonElement"):
"""GIS Polygon element"""
geometry: Optional[List[GisPolygonGeometry]] = None
attributes: Optional[Base] = None
class GisLineElement(Base, speckle_type="Objects.GIS.LineElement"):
"""GIS Polyline element"""
geometry: Optional[List[Union[Polyline, Arc, Line, Circle, Polycurve]]] = None
attributes: Optional[Base] = None
class GisPointElement(Base, speckle_type="Objects.GIS.PointElement"):
"""GIS Point element"""
geometry: Optional[List[Point]] = None
attributes: Optional[Base] = None
class GisRasterElement(
Base, speckle_type="Objects.GIS.RasterElement", detachable={"displayValue"}
):
"""GIS Raster element"""
band_count: Optional[int] = None
band_names: Optional[List[str]] = None
x_origin: Optional[float] = None
y_origin: Optional[float] = None
x_size: Optional[int] = None
y_size: Optional[int] = None
x_resolution: Optional[float] = None
y_resolution: Optional[float] = None
noDataValue: Optional[List[float]] = None
displayValue: Optional[List[Mesh]] = None
class GisTopography(
GisRasterElement,
speckle_type="Objects.GIS.GisTopography",
detachable={"displayValue"},
):
"""GIS Raster element with 3d Topography representation"""
class GisNonGeometryElement(Base, speckle_type="Objects.GIS.NonGeometryElement"):
"""GIS Table feature"""
attributes: Optional[Base] = None
-142
View File
@@ -1,142 +0,0 @@
from typing import Any, Dict, List, Optional, Union
from deprecated import deprecated
from specklepy.objects.base import Base
from specklepy.objects.GIS.CRS import CRS
from specklepy.objects.other import Collection
@deprecated(version="2.15", reason="Use VectorLayer or RasterLayer instead")
class Layer(Base, detachable={"features"}):
"""A GIS Layer"""
def __init__(
self,
name: Optional[str] = None,
crs: Optional[CRS] = None,
units: str = "m",
features: Optional[List[Base]] = None,
layerType: str = "None",
geomType: str = "None",
renderer: Optional[Dict[str, Any]] = None,
**kwargs,
) -> None:
super().__init__(**kwargs)
self.name = name
self.crs = crs
self.units = units
self.type = layerType
self.features = features or []
self.geomType = geomType
self.renderer = renderer or {}
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class VectorLayer(
Collection,
detachable={"elements"},
speckle_type="VectorLayer",
serialize_ignore={"features"},
):
"""GIS Vector Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
elements: Optional[List[Base]] = None
attributes: Optional[Base] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
@deprecated(version="2.16", reason="Use VectorLayer or RasterLayer instead")
class RasterLayer(
Collection,
detachable={"elements"},
speckle_type="RasterLayer",
serialize_ignore={"features"},
):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]] = None
elements: Optional[List[Base]] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class VectorLayer( # noqa: F811
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.VectorLayer",
serialize_ignore={"features"},
):
"""GIS Vector Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
elements: Optional[List[Base]] = None
attributes: Optional[Base] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "VectorLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
class RasterLayer( # noqa: F811
Collection,
detachable={"elements"},
speckle_type="Objects.GIS.RasterLayer",
serialize_ignore={"features"},
):
"""GIS Raster Layer"""
name: Optional[str] = None
crs: Optional[Union[CRS, Base]] = None
units: Optional[str] = None
rasterCrs: Optional[Union[CRS, Base]] = None
elements: Optional[List[Base]] = None
geomType: Optional[str] = "None"
renderer: Optional[Dict[str, Any]] = None
collectionType = "RasterLayer"
@property
@deprecated(version="2.14", reason="Use elements")
def features(self) -> Optional[List[Base]]:
return self.elements
@features.setter
def features(self, value: Optional[List[Base]]) -> None:
self.elements = value
+3 -20
View File
@@ -1,23 +1,6 @@
"""Builtin Speckle object kit."""
from specklepy.objects import (
GIS,
encoding,
geometry,
other,
primitive,
structural,
units,
)
from specklepy.objects.base import Base
from .data_objects import DataObject, QgisObject
__all__ = [
"Base",
"encoding",
"geometry",
"other",
"units",
"structural",
"primitive",
"GIS",
"DataObject",
"QgisObject",
]
+35 -36
View File
@@ -1,4 +1,5 @@
import contextlib
from dataclasses import dataclass, field
from enum import Enum
from inspect import isclass
from typing import (
@@ -18,8 +19,7 @@ from warnings import warn
from stringcase import pascalcase
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.units import Units
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool)
@@ -65,7 +65,6 @@ REMOVE_FROM_DIR = {
"_handle_object_count",
"_type_check",
"_type_registry",
"_units",
"add_chunkable_attrs",
"add_detachable_attrs",
"get_children_count",
@@ -116,7 +115,7 @@ class _RegisteringBase:
@classmethod
def _determine_speckle_type(cls) -> str:
"""
This method brings the speckle_type construction in par with peckle-sharp/Core.
This method brings the speckle_type construction in par with Speckle-sharp/Core.
The implementation differs, because in Core the basis of the speckle_type if
type.FullName, which includes the dotnet namespace name too.
@@ -168,8 +167,11 @@ class _RegisteringBase:
initialization. This is reused to register each subclassing type into a class
level dictionary.
"""
# if not speckle_type:
# raise Exception("no type")
cls._speckle_type_override = speckle_type
cls.speckle_type = cls._determine_speckle_type()
# cls.speckle_type = speckle_type
if cls._full_name() in cls._type_registry:
raise ValueError(
f"The speckle_type: {speckle_type} is already registered for type: "
@@ -222,7 +224,7 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if isinstance(t, ForwardRef):
return True, value
origin = getattr(t, "__origin__")
origin = t.__origin__
# below is what in nicer for >= py38
# origin = get_origin(t)
@@ -287,7 +289,7 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
if len(args) != len(value):
return False, value
values = []
for t_item, v_item in zip(args, value):
for t_item, v_item in zip(args, value, strict=True):
item_valid, item_value = _validate_type(t_item, v_item)
if not item_valid:
return False, value
@@ -319,22 +321,17 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
return False, value
class Base(_RegisteringBase):
@dataclass(kw_only=True)
class Base(_RegisteringBase, speckle_type="Base"):
id: Union[str, None] = None
totalChildrenCount: Union[int, None] = None
# totalChildrenCount: Union[int, None] = None
applicationId: Union[str, None] = None
_units: Union[None, str] = None
def __init__(self, **kwargs) -> None:
super().__init__()
for k, v in kwargs.items():
self.__setattr__(k, v)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(id: {self.id}, "
f"speckle_type: {self.speckle_type}, "
f"totalChildrenCount: {self.totalChildrenCount})"
# f"totalChildrenCount: {self.totalChildrenCount})"
)
def __str__(self) -> str:
@@ -375,7 +372,8 @@ class Base(_RegisteringBase):
if name == "speckle_type":
# not sure if we should raise an exception here??
# raise SpeckleException(
# "Cannot override the `speckle_type`. This is set manually by the class or on deserialisation"
# "Cannot override the `speckle_type`."
# "This is set manually by the class or on deserialisation"
# )
return
# if value is not None:
@@ -403,7 +401,10 @@ class Base(_RegisteringBase):
try:
cls._attr_types = get_type_hints(cls)
except Exception as e:
warn(f"Could not update forward refs for class {cls.__name__}: {e}")
warn(
f"Could not update forward refs for class {cls.__name__}: {e}",
stacklevel=2,
)
@classmethod
def validate_prop_name(cls, name: str) -> None:
@@ -462,21 +463,22 @@ class Base(_RegisteringBase):
"""
self._detachable = self._detachable.union(names)
@property
def units(self) -> Union[str, None]:
return self._units
# @property
# def units(self) -> Union[str, None]:
# return self._units
@units.setter
def units(self, value: Union[str, Units, None]):
"""While this property accepts any string value, geometry expects units to be specific strings (see Units enum)"""
if isinstance(value, str) or value is None:
self._units = value
elif isinstance(value, Units):
self._units = value.value
else:
raise SpeckleInvalidUnitException(
f"Unknown type {type(value)} received for units"
)
# @units.setter
# def units(self, value: Union[str, Units, None]):
# """While this property accepts any string value,
# geometry expects units to be specific strings (see Units enum)"""
# if isinstance(value, str) or value is None:
# self._units = value
# elif isinstance(value, Units):
# self._units = value.value
# else:
# raise SpeckleInvalidUnitException(
# f"Unknown type {type(value)} received for units"
# )
def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not"""
@@ -568,9 +570,6 @@ class Base(_RegisteringBase):
Base.update_forward_refs()
@dataclass(kw_only=True)
class DataChunk(Base, speckle_type="Speckle.Core.Models.DataChunk"):
data: Union[List[Any], None] = None
def __init__(self) -> None:
super().__init__()
self.data = []
data: List[Any] = field(default_factory=list)
+81
View File
@@ -0,0 +1,81 @@
from dataclasses import dataclass, field
from typing import Dict, List
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IDataObject, IGisObject, IHasUnits
from specklepy.logging.exceptions import SpeckleException
@dataclass(kw_only=True)
class DataObject(
Base,
IDataObject,
speckle_type="Objects.Data.DataObject",
detachable={"displayValue"},
):
name: str
properties: Dict[str, object]
displayValue: List[Base]
_name: str = field(repr=False, init=False)
_properties: Dict[str, object] = field(repr=False, init=False)
_displayValue: List[Base] = field(repr=False, init=False)
@property
def name(self) -> str:
return self._name
@property
def properties(self) -> Dict[str, object]:
return self._properties
@property
def displayValue(self) -> List[Base]:
return self._displayValue
@name.setter
def name(self, value: str):
if isinstance(value, str):
self._name = value
else:
raise SpeckleException(
f"'name' value should be string, received {type(value)}"
)
@properties.setter
def properties(self, value: dict):
if isinstance(value, dict):
self._properties = value
else:
raise SpeckleException(
f"'properties' value should be Dict, received {type(value)}"
)
@displayValue.setter
def displayValue(self, value: list):
if isinstance(value, list):
self._displayValue = value
else:
raise SpeckleException(
f"'displayValue' value should be List, received {type(value)}"
)
@dataclass(kw_only=True)
class QgisObject(
DataObject, IGisObject, IHasUnits, speckle_type="Objects.Data.QgisObject"
):
type: str
_type: str = field(repr=False, init=False)
@property
def type(self) -> str:
return self._type
@type.setter
def type(self, value: str):
if isinstance(value, str):
self._type = value
else:
raise SpeckleException(
f"'type' value should be string, received {type(value)}"
)
-131
View File
@@ -1,131 +0,0 @@
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Type
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
class CurveTypeEncoding(int, Enum):
Arc = 0
Circle = 1
Curve = 2
Ellipse = 3
Line = 4
Polyline = 5
Polycurve = 6
@property
def object_class(self) -> Type:
from . import geometry
if self == self.Arc:
return geometry.Arc
elif self == self.Circle:
return geometry.Circle
elif self == self.Curve:
return geometry.Curve
elif self == self.Ellipse:
return geometry.Ellipse
elif self == self.Line:
return geometry.Line
elif self == self.Polyline:
return geometry.Polyline
elif self == self.Polycurve:
return geometry.Polycurve
raise SpeckleException(
f"No corresponding object class for CurveTypeEncoding: {self}"
)
def curve_from_list(args: List[float]):
curve_type = CurveTypeEncoding(args[0])
return curve_type.object_class.from_list(args)
class ObjectArray:
def __init__(self, data: Optional[list] = None) -> None:
self.data = data or []
@classmethod
def from_objects(cls, objects: List[Base]) -> "ObjectArray":
data_list = cls()
if not objects:
return data_list
speckle_type = objects[0].speckle_type
for obj in objects:
if speckle_type != obj.speckle_type:
raise SpeckleException(
"All objects in chunk should have the same speckle_type. "
f"Found {speckle_type} and {obj.speckle_type}"
)
data_list.encode_object(obj=obj)
return data_list
@staticmethod
def decode_data(
data: List[Any], decoder: Callable[[List[Any]], Base], **kwargs: Dict[str, Any]
) -> List[Base]:
bases: List[Base] = []
if not data:
return bases
index = 0
while index < len(data):
item_length = int(data[index])
item_start = index + 1
item_end = item_start + item_length
item_data = data[item_start:item_end]
index = item_end
decoded_data = decoder(item_data, **kwargs)
bases.append(decoded_data)
return bases
def decode(self, decoder: Callable[[List[Any]], Any], **kwargs: Dict[str, Any]):
return self.decode_data(data=self.data, decoder=decoder, **kwargs)
def encode_object(self, obj: Base):
encoded = obj.to_list()
encoded.insert(0, len(encoded))
self.data.extend(encoded)
class CurveArray(ObjectArray):
@classmethod
def from_curve(cls, curve: Base) -> "CurveArray":
crv_array = cls()
crv_array.data = curve.to_list()
return crv_array
@classmethod
def from_curves(cls, curves: List[Base]) -> "CurveArray":
data = []
for curve in curves:
curve_list = curve.to_list()
curve_list.insert(0, len(curve_list))
data.extend(curve_list)
crv_array = cls()
crv_array.data = data
return crv_array
@staticmethod
def curve_from_list(args: List[float]) -> Base:
curve_type = CurveTypeEncoding(args[0])
return curve_type.object_class.from_list(args)
@property
def type(self) -> CurveTypeEncoding:
return CurveTypeEncoding(self.data[0])
def to_curve(self) -> Base:
return self.type.object_class.from_list(self.data)
@classmethod
def _curve_decoder(cls, data: List[float]) -> Base:
crv_array = cls(data)
return crv_array.to_curve()
def to_curves(self) -> List[Base]:
return self.decode(decoder=self._curve_decoder)
-946
View File
@@ -1,946 +0,0 @@
from enum import Enum
from typing import Any, List, Optional
from specklepy.objects.base import Base
from specklepy.objects.encoding import CurveArray, CurveTypeEncoding, ObjectArray
from specklepy.objects.primitive import Interval
from specklepy.objects.units import get_encoding_from_units, get_units_from_encoding
GEOMETRY = "Objects.Geometry."
class Point(Base, speckle_type=GEOMETRY + "Point"):
x: float = 0.0
y: float = 0.0
z: float = 0.0
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id:"
f" {self.id}, speckle_type: {self.speckle_type})"
)
@classmethod
def from_list(cls, args: List[float]) -> "Point":
"""
Create a new Point from a list of three floats
representing the x, y, and z coordinates
"""
return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[Any]:
return [self.x, self.y, self.z]
@classmethod
def from_coords(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0):
"""Create a new Point from x, y, and z values"""
pt = Point()
pt.x, pt.y, pt.z = x, y, z
return pt
class Pointcloud(
Base,
speckle_type=GEOMETRY + "Pointcloud",
chunkable={"points": 31250, "colors": 62500, "sizes": 62500},
):
points: Optional[List[float]] = None
colors: Optional[List[int]] = None
sizes: Optional[List[float]] = None
bbox: Optional["Box"] = None
class Vector(Base, speckle_type=GEOMETRY + "Vector"):
x: float = 0.0
y: float = 0.0
z: float = 0.0
applicationId: Optional[str] = None
def __repr__(self) -> str:
return (
f"{self.__class__.__name__} "
"(x: {self.x}, y: {self.y}, z: {self.z}, id: {self.id}, "
"speckle_type: {self.speckle_type})"
)
@classmethod
def from_list(cls, args: List[float]) -> "Vector":
"""
Create from a list of three floats representing the x, y, and z coordinates.
"""
return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[float]:
return [self.x, self.y, self.z]
@classmethod
def from_coords(cls, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> "Vector":
"""Create a new Point from x, y, and z values"""
v = Vector()
v.x, v.y, v.z = x, y, z
return v
class ControlPoint(Point, speckle_type=GEOMETRY + "ControlPoint"):
weight: Optional[float] = None
class Plane(Base, speckle_type=GEOMETRY + "Plane"):
origin: Point = Point()
normal: Vector = Vector()
xdir: Vector = Vector()
ydir: Vector = Vector()
@classmethod
def from_list(cls, args: List[Any]) -> "Plane":
return cls(
origin=Point.from_list(args[:3]),
normal=Vector.from_list(args[3:6]),
xdir=Vector.from_list(args[6:9]),
ydir=Vector.from_list(args[9:12]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
*self.origin.to_list(),
*self.normal.to_list(),
*self.xdir.to_list(),
*self.ydir.to_list(),
get_encoding_from_units(self._units),
]
class Box(Base, speckle_type=GEOMETRY + "Box"):
basePlane: Plane = Plane()
xSize: Interval = Interval()
ySize: Interval = Interval()
zSize: Interval = Interval()
area: Optional[float] = None
volume: Optional[float] = None
class Line(Base, speckle_type=GEOMETRY + "Line"):
start: Point = Point()
end: Optional[Point] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Line":
return cls(
start=Point.from_list(args[1:4]),
end=Point.from_list(args[4:7]),
domain=Interval.from_list(args[7:10]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
domain = self.domain.to_list() if self.domain else [0, 1]
return [
CurveTypeEncoding.Line.value,
*self.start.to_list(),
*self.end.to_list(),
*domain,
get_encoding_from_units(self._units),
]
class Arc(Base, speckle_type=GEOMETRY + "Arc"):
radius: Optional[float] = None
startAngle: Optional[float] = None
endAngle: Optional[float] = None
angleRadians: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
startPoint: Optional[Point] = None
midPoint: Optional[Point] = None
endPoint: Optional[Point] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Arc":
return cls(
radius=args[1],
startAngle=args[2],
endAngle=args[3],
angleRadians=args[4],
domain=Interval.from_list(args[5:7]),
plane=Plane.from_list(args[7:20]),
startPoint=Point.from_list(args[20:23]),
midPoint=Point.from_list(args[23:26]),
endPoint=Point.from_list(args[26:29]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Arc.value,
self.radius,
self.startAngle,
self.endAngle,
self.angleRadians,
*self.domain.to_list(),
*self.plane.to_list(),
*self.startPoint.to_list(),
*self.midPoint.to_list(),
*self.endPoint.to_list(),
get_encoding_from_units(self._units),
]
class Circle(Base, speckle_type=GEOMETRY + "Circle"):
radius: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Circle":
return cls(
radius=args[1],
domain=Interval.from_list(args[2:4]),
plane=Plane.from_list(args[4:17]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Circle.value,
self.radius,
*self.domain.to_list(),
*self.plane.to_list(),
get_encoding_from_units(self._units),
]
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
firstRadius: Optional[float] = None
secondRadius: Optional[float] = None
plane: Optional[Plane] = None
domain: Optional[Interval] = None
trimDomain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Ellipse":
return cls(
firstRadius=args[1],
secondRadius=args[2],
domain=Interval.from_list(args[3:5]),
plane=Plane.from_list(args[5:18]),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Ellipse.value,
self.firstRadius,
self.secondRadius,
*self.domain.to_list(),
*self.plane.to_list(),
get_encoding_from_units(self._units),
]
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
value: Optional[List[float]] = None
closed: Optional[bool] = None
domain: Optional[Interval] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_points(cls, points: List[Point]):
"""Create a new Polyline from a list of Points"""
polyline = cls()
polyline.units = points[0].units
polyline.value = []
for point in points:
polyline.value.extend([point.x, point.y, point.z])
return polyline
@classmethod
def from_list(cls, args: List[Any]) -> "Polyline":
point_count = args[4]
return cls(
closed=bool(args[1]),
domain=Interval.from_list(args[2:4]),
value=args[5 : 5 + point_count],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Polyline.value,
int(self.closed),
*self.domain.to_list(),
len(self.value),
*self.value,
get_encoding_from_units(self._units),
]
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.value:
return
if len(self.value) % 3:
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.value)
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
class SpiralType(Enum):
Biquadratic = 0
BiquadraticParabola = 1
Bloss = 2
Clothoid = 3
Cosine = 4
Cubic = 5
CubicParabola = 6
Radioid = 7
Sinusoid = 8
Unknown = 9
class Spiral(Base, speckle_type=GEOMETRY + "Spiral", detachable={"displayValue"}):
startPoint: Optional[Point] = None
endPoint: Optional[Point]
plane: Optional[Plane]
turns: Optional[float]
pitchAxis: Optional[Vector] = Vector()
pitch: float = 0
spiralType: Optional[SpiralType] = None
displayValue: Optional[Polyline] = None
bbox: Optional[Box] = None
length: Optional[float] = None
domain: Optional[Interval] = None
class Curve(
Base,
speckle_type=GEOMETRY + "Curve",
chunkable={"points": 20000, "weights": 20000, "knots": 20000},
):
degree: Optional[int] = None
periodic: Optional[bool] = None
rational: Optional[bool] = None
points: Optional[List[float]] = None
weights: Optional[List[float]] = None
knots: Optional[List[float]] = None
domain: Optional[Interval] = None
displayValue: Optional[Polyline] = None
closed: Optional[bool] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points"""
if not self.points:
return
if len(self.points) % 3:
raise ValueError("Points array malformed: length%3 != 0.")
values = iter(self.points)
return [
Point(x=v, y=next(values), z=next(values), units=self.units) for v in values
]
@classmethod
def from_list(cls, args: List[Any]) -> "Curve":
point_count = int(args[7])
weights_count = int(args[8])
knots_count = int(args[9])
points_start = 10
weights_start = 10 + point_count
knots_start = weights_start + weights_count
knots_end = knots_start + knots_count
return cls(
degree=int(args[1]),
periodic=bool(args[2]),
rational=bool(args[3]),
closed=bool(args[4]),
domain=Interval.from_list(args[5:7]),
points=args[points_start:weights_start],
weights=args[weights_start:knots_start],
knots=args[knots_start:knots_end],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
CurveTypeEncoding.Curve.value,
self.degree,
int(self.periodic),
int(self.rational),
int(self.closed),
*self.domain.to_list(),
len(self.points),
len(self.weights),
len(self.knots),
*self.points,
*self.weights,
*self.knots,
get_encoding_from_units(self._units),
]
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: Optional[List[Base]] = None
domain: Optional[Interval] = None
closed: Optional[bool] = None
bbox: Optional[Box] = None
area: Optional[float] = None
length: Optional[float] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Polycurve":
curve_arrays = CurveArray(args[5:-1])
return cls(
closed=bool(args[1]),
domain=Interval.from_list(args[2:4]),
segments=curve_arrays.to_curves(),
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
curve_array = CurveArray.from_curves(self.segments).data
return [
CurveTypeEncoding.Polycurve.value,
int(self.closed),
*self.domain.to_list(),
len(curve_array),
*curve_array,
get_encoding_from_units(self._units),
]
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
capped: Optional[bool] = None
profile: Optional[Base] = None
pathStart: Optional[Point] = None
pathEnd: Optional[Point] = None
pathCurve: Optional[Base] = None
pathTangent: Optional[Base] = None
profiles: Optional[List[Base]] = None
length: Optional[float] = None
area: Optional[float] = None
volume: Optional[float] = None
bbox: Optional[Box] = None
class Mesh(
Base,
speckle_type=GEOMETRY + "Mesh",
chunkable={
"vertices": 2000,
"faces": 2000,
"colors": 2000,
"textureCoordinates": 2000,
},
):
vertices: Optional[List[float]] = None
faces: Optional[List[int]] = None
colors: Optional[List[int]] = None
textureCoordinates: Optional[List[float]] = None
bbox: Optional[Box] = None
area: Optional[float] = None
volume: Optional[float] = None
@classmethod
def create(
cls,
vertices: List[float],
faces: List[int],
colors: Optional[List[int]] = None,
texture_coordinates: Optional[List[float]] = None,
) -> "Mesh":
"""
Create a new Mesh from lists representing its vertices, faces,
colors (optional), and texture coordinates (optional).
This will initialise empty lists for colors and texture coordinates
if you do not provide any.
"""
return cls(
vertices=vertices,
faces=faces,
colors=colors or [],
textureCoordinates=texture_coordinates or [],
)
class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: Optional[int] = None
degreeV: Optional[int] = None
rational: Optional[bool] = None
area: Optional[float] = None
pointData: Optional[List[float]] = None
countU: Optional[int] = None
countV: Optional[int] = None
bbox: Optional[Box] = None
closedU: Optional[bool] = None
closedV: Optional[bool] = None
domainU: Optional[Interval] = None
domainV: Optional[Interval] = None
knotsU: Optional[List[float]] = None
knotsV: Optional[List[float]] = None
@classmethod
def from_list(cls, args: List[Any]) -> "Surface":
point_count = int(args[11])
knots_u_count = int(args[12])
knots_v_count = int(args[13])
start_point_data = 14
start_knots_u = start_point_data + point_count
start_knots_v = start_knots_u + knots_u_count
return cls(
degreeU=int(args[0]),
degreeV=int(args[1]),
countU=int(args[2]),
countV=int(args[3]),
rational=bool(args[4]),
closedU=bool(args[5]),
closedV=bool(args[6]),
domainU=Interval(start=args[7], end=args[8]),
domainV=Interval(start=args[9], end=args[10]),
pointData=args[start_point_data:start_knots_u],
knotsU=args[start_knots_u:start_knots_v],
knotsV=args[start_knots_v : start_knots_v + knots_v_count],
units=get_units_from_encoding(args[-1]),
)
def to_list(self) -> List[Any]:
return [
self.degreeU,
self.degreeV,
self.countU,
self.countV,
int(self.rational),
int(self.closedU),
int(self.closedV),
*self.domainU.to_list(),
*self.domainV.to_list(),
len(self.pointData),
len(self.knotsU),
len(self.knotsV),
*self.pointData,
*self.knotsU,
*self.knotsV,
get_encoding_from_units(self._units),
]
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
_Brep: Optional["Brep"] = None
SurfaceIndex: Optional[int] = None
OuterLoopIndex: Optional[int] = None
OrientationReversed: Optional[bool] = None
LoopIndices: Optional[List[int]] = None
@property
def _outer_loop(self):
return self._Brep.Loops[self.OuterLoopIndex] # pylint: disable=no-member
@property
def _surface(self):
return self._Brep.Surfaces[self.SurfaceIndex] # pylint: disable=no-member
@property
def _loops(self):
if self.LoopIndices:
# pylint: disable=not-an-iterable, no-member
return [self._Brep.Loops[i] for i in self.LoopIndices]
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepFace":
return cls(
_Brep=brep,
SurfaceIndex=args[0],
OuterLoopIndex=args[1],
OrientationReversed=bool(args[2]),
LoopIndices=args[3:],
)
def to_list(self) -> List[Any]:
return [
self.SurfaceIndex,
self.OuterLoopIndex,
int(self.OrientationReversed),
*self.LoopIndices,
]
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
_Brep: Optional["Brep"] = None
Curve3dIndex: Optional[int] = None
TrimIndices: Optional[List[int]] = None
StartIndex: Optional[int] = None
EndIndex: Optional[int] = None
ProxyCurveIsReversed: Optional[bool] = None
Domain: Optional[Interval] = None
@property
def _start_vertex(self):
return self._Brep.Vertices[self.StartIndex]
@property
def _end_vertex(self):
return self._Brep.Vertices[self.EndIndex]
@property
def _trims(self):
if self.TrimIndices:
# pylint: disable=not-an-iterable
return [self._Brep.Trims[i] for i in self.TrimIndices]
@property
def _curve(self):
return self._Brep.Curve3D[self.Curve3dIndex]
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepEdge":
domain_start = args[4]
domain_end = args[5]
domain = (
Interval(start=domain_start, end=domain_end)
if None not in (domain_start, domain_end)
else None
)
return cls(
_Brep=brep,
Curve3dIndex=int(args[0]),
TrimIndices=[int(t) for t in args[6:]],
StartIndex=int(args[1]),
EndIndex=int(args[2]),
ProxyCurveIsReversed=bool(args[3]),
Domain=domain,
)
def to_list(self) -> List[Any]:
return [
self.Curve3dIndex,
self.StartIndex,
self.EndIndex,
int(self.ProxyCurveIsReversed),
self.Domain.start,
self.Domain.end,
*self.TrimIndices,
]
class BrepLoopType(int, Enum):
Unknown = 0
Outer = 1
Inner = 2
Slit = 3
CurveOnSurface = 4
PointOnSurface = 5
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
_Brep: Optional["Brep"] = None
FaceIndex: Optional[Optional[int]] = None
TrimIndices: Optional[List[int]] = None
Type: Optional[BrepLoopType] = None
@property
def _face(self):
return self._Brep.Faces[self.FaceIndex]
@property
def _trims(self):
if self.TrimIndices:
# pylint: disable=not-an-iterable
return [self._Brep.Trims[i] for i in self.TrimIndices]
@classmethod
def from_list(cls, args: List[any], brep: "Brep" = None):
return cls(
_Brep=brep,
FaceIndex=args[0],
Type=BrepLoopType(args[1]),
TrimIndices=args[2:],
)
def to_list(self) -> List[int]:
return [
self.FaceIndex,
self.Type.value,
*self.TrimIndices,
]
class BrepTrimType(int, Enum):
Unknown = 0
Boundary = 1
Mated = 2
Seam = 3
Singular = 4
CurveOnSurface = 5
PointOnSurface = 6
Slit = 7
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
_Brep: Optional["Brep"] = None
EdgeIndex: Optional[int] = None
StartIndex: Optional[int] = None
EndIndex: Optional[int] = None
FaceIndex: Optional[int] = None
LoopIndex: Optional[int] = None
CurveIndex: Optional[int] = None
IsoStatus: Optional[int] = None
TrimType: Optional[BrepTrimType] = None
IsReversed: Optional[bool] = None
Domain: Optional[Interval] = None
@property
def _face(self):
if self._Brep:
return self._Brep.Faces[self.FaceIndex] # pylint: disable=no-member
@property
def _loop(self):
if self._Brep:
return self._Brep.Loops[self.LoopIndex] # pylint: disable=no-member
@property
def _edge(self):
if self._Brep:
# pylint: disable=no-member
return self._Brep.Edges[self.EdgeIndex] if self.EdgeIndex != -1 else None
@property
def _curve_2d(self):
if self._Brep:
return self._Brep.Curve2D[self.CurveIndex] # pylint: disable=no-member
@classmethod
def from_list(cls, args: List[Any], brep: "Brep" = None) -> "BrepTrim":
return cls(
_Brep=brep,
EdgeIndex=args[0],
StartIndex=args[1],
EndIndex=args[2],
FaceIndex=args[3],
LoopIndex=args[4],
CurveIndex=args[5],
IsoStatus=args[6],
TrimType=BrepTrimType(args[7]),
IsReversed=bool(args[8]),
)
def to_list(self) -> List[Any]:
return [
self.EdgeIndex,
self.StartIndex,
self.EndIndex,
self.FaceIndex,
self.LoopIndex,
self.CurveIndex,
self.IsoStatus,
self.TrimType.value,
int(self.IsReversed),
]
class Brep(
Base,
speckle_type=GEOMETRY + "Brep",
chunkable={
"SurfacesValue": 31250,
"Curve3DValues": 31250,
"Curve2DValues": 31250,
"VerticesValue": 31250,
"EdgesValue": 62500,
"LoopsValue": 62500,
"FacesValue": 62500,
"TrimsValue": 62500,
},
detachable={"displayValue"},
serialize_ignore={
"Surfaces",
"Curve3D",
"Curve2D",
"Vertices",
"Trims",
"Edges",
"Loops",
"Faces",
},
):
provenance: Optional[str] = None
bbox: Optional[Box] = None
area: Optional[float] = None
volume: Optional[float] = None
_displayValue: Optional[List[Mesh]] = None
Surfaces: Optional[List[Surface]] = None
Curve3D: Optional[List[Base]] = None
Curve2D: Optional[List[Base]] = None
Vertices: Optional[List[Point]] = None
Edges: Optional[List[BrepEdge]] = None
Loops: Optional[List[BrepLoop]] = None
Faces: Optional[List[BrepFace]] = None
Trims: Optional[List[BrepTrim]] = None
IsClosed: Optional[bool] = None
Orientation: Optional[int] = None
def _inject_self_into_children(self, children: Optional[List[Base]]) -> List[Base]:
if children is None:
return children
for child in children:
child._Brep = self # pylint: disable=protected-access
return children
# set as prop for now for backwards compatibility
@property
def displayValue(self) -> List[Mesh]:
return self._displayValue
@displayValue.setter
def displayValue(self, value):
if isinstance(value, Mesh):
self._displayValue = [value]
elif isinstance(value, list):
self._displayValue = value
@property
def EdgesValue(self) -> List[BrepEdge]:
return None if self.Edges is None else ObjectArray.from_objects(self.Edges).data
@EdgesValue.setter
def EdgesValue(self, value: List[float]):
if not value:
return
self.Edges = ObjectArray.decode_data(value, BrepEdge.from_list, brep=self)
@property
def LoopsValue(self) -> List[BrepLoop]:
return None if self.Loops is None else ObjectArray.from_objects(self.Loops).data
@LoopsValue.setter
def LoopsValue(self, value: List[int]):
if not value:
return
self.Loops = ObjectArray.decode_data(value, BrepLoop.from_list, brep=self)
@property
def FacesValue(self) -> List[int]:
return None if self.Faces is None else ObjectArray.from_objects(self.Faces).data
@FacesValue.setter
def FacesValue(self, value: List[int]):
if not value:
return
self.Faces = ObjectArray.decode_data(value, BrepFace.from_list, brep=self)
@property
def SurfacesValue(self) -> List[float]:
return (
None
if self.Surfaces is None
else ObjectArray.from_objects(self.Surfaces).data
)
@SurfacesValue.setter
def SurfacesValue(self, value: List[float]):
if not value:
return
self.Surfaces = ObjectArray.decode_data(value, Surface.from_list)
@property
def Curve3DValues(self) -> List[float]:
return (
None if self.Curve3D is None else CurveArray.from_curves(self.Curve3D).data
)
@Curve3DValues.setter
def Curve3DValues(self, value: List[float]):
crv_array = CurveArray(value)
self.Curve3D = crv_array.to_curves()
@property
def Curve2DValues(self) -> List[Base]:
return (
None if self.Curve2D is None else CurveArray.from_curves(self.Curve2D).data
)
@Curve2DValues.setter
def Curve2DValues(self, value: List[float]):
crv_array = CurveArray(value)
self.Curve2D = crv_array.to_curves()
@property
def VerticesValue(self) -> List[Point]:
if self.Vertices is None:
return None
encoded_unit = get_encoding_from_units(self.Vertices[0].units)
values = [encoded_unit]
for vertex in self.Vertices:
values.extend(vertex.to_list())
return values
@VerticesValue.setter
def VerticesValue(self, value: List[float]):
value = value.copy()
units = get_units_from_encoding(value.pop(0))
vertices = []
for i in range(0, len(value), 3):
vertex = Point.from_list(value[i : i + 3])
vertex.units = units
vertices.append(vertex)
self.Vertices = vertices
# TODO: can this be consistent with loops, edges, faces, curves, etc and prepend with the chunk list? needs to happen in sharp first
@property
def TrimsValue(self) -> List[float]:
# return None if self.Trims is None else ObjectArray.from_objects(self.Trims).data
if not self.Trims:
return
value = []
for trim in self.Trims:
value.extend(trim.to_list())
return value
@TrimsValue.setter
def TrimsValue(self, value: List[float]):
if not value:
return
# self.Trims = ObjectArray.decode_data(value, BrepTrim.from_list, brep=self)
self.Trims = [
BrepTrim.from_list(value[i : i + 9], self) for i in range(0, len(value), 9)
]
BrepEdge.update_forward_refs()
BrepLoop.update_forward_refs()
BrepTrim.update_forward_refs()
BrepFace.update_forward_refs()
@@ -0,0 +1,18 @@
from specklepy.objects.geometry.arc import Arc
from specklepy.objects.geometry.line import Line
from specklepy.objects.geometry.mesh import Mesh
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.polyline import Polyline
from specklepy.objects.geometry.vector import Vector
# re-export them at the geometry package level
__all__ = [
"Arc",
"Line",
"Mesh",
"Plane",
"Point",
"Polyline",
"Vector"
]
+36
View File
@@ -0,0 +1,36 @@
import math
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.geometry.plane import Plane
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import ICurve, IHasUnits
@dataclass(kw_only=True)
class Arc(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Arc"):
plane: Plane
startPoint: Point
midPoint: Point
endPoint: Point
@property
def radius(self) -> float:
return self.startPoint.distance_to(self.plane.origin)
@property
def length(self) -> float:
start_to_mid = self.startPoint.distance_to(self.midPoint)
mid_to_end = self.midPoint.distance_to(self.endPoint)
r = self.radius
angle = (2 * math.asin(start_to_mid / (2 * r))) + \
(2 * math.asin(mid_to_end / (2 * r)))
return r * angle
@property
def measure(self) -> float:
start_to_mid = self.startPoint.distance_to(self.midPoint)
mid_to_end = self.midPoint.distance_to(self.endPoint)
r = self.radius
return (2 * math.asin(start_to_mid / (2 * r))) + \
(2 * math.asin(mid_to_end / (2 * r)))
+20
View File
@@ -0,0 +1,20 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import ICurve, IHasUnits
@dataclass(kw_only=True)
class Line(
Base,
IHasUnits,
ICurve,
speckle_type="Objects.Geometry.Line"
):
start: Point
end: Point
@property
def length(self) -> float:
return self.start.distance_to(self.end)
+211
View File
@@ -0,0 +1,211 @@
from dataclasses import dataclass, field
from typing import List, Tuple
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import IHasArea, IHasUnits, IHasVolume
@dataclass(kw_only=True)
class Mesh(
Base,
IHasArea,
IHasVolume,
IHasUnits,
speckle_type="Objects.Geometry.Mesh",
detachable={"vertices", "faces", "colors", "textureCoordinates"},
chunkable={
"vertices": 31250,
"faces": 62500,
"colors": 62500,
"textureCoordinates": 31250,
},
serialize_ignore={"vertices_count", "texture_coordinates_count"},
):
"""
a 3D mesh consisting of vertices and faces with optional colors and texture coordinates
"""
vertices: List[float]
faces: List[int]
colors: List[int] = field(default_factory=list)
textureCoordinates: List[float] = field(default_factory=list)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"vertices: {self.vertices_count}, "
f"faces: {self.faces_count}, "
f"units: {self.units}, "
f"has_colors: {len(self.colors) > 0}, "
f"has_texture_coords: {len(self.textureCoordinates) > 0})"
)
@property
def vertices_count(self) -> int:
"""
get the number of vertices in the mesh
"""
if len(self.vertices) % 3 != 0:
raise ValueError(
f"Invalid vertices list: length ({len(
self.vertices)}) must be a multiple of 3"
)
return len(self.vertices) // 3
@property
def texture_coordinates_count(self) -> int:
"""
get the number of texture coordinates in the mesh
"""
return len(self.textureCoordinates) // 2
@property
def area(self) -> float:
return self.__dict__.get('_area', 0.0)
@area.setter
def area(self, value: float) -> None:
self.__dict__['_area'] = value
@property
def volume(self) -> float:
return self.__dict__.get('_volume', 0.0)
@volume.setter
def volume(self, value: float) -> None:
self.__dict__['_volume'] = value
def calculate_area(self) -> float:
"""
calculate total surface area of the mesh
"""
total_area = 0.0
face_index = 0
i = 0
while i < len(self.faces):
vertex_count = self.faces[i]
if vertex_count >= 3:
face_vertices = self.get_face_vertices(face_index)
for j in range(1, vertex_count - 1):
v0 = face_vertices[0]
v1 = face_vertices[j]
v2 = face_vertices[j + 1]
a = [v1.x - v0.x, v1.y - v0.y, v1.z - v0.z]
b = [v2.x - v0.x, v2.y - v0.y, v2.z - v0.z]
cx = a[1] * b[2] - a[2] * b[1]
cy = a[2] * b[0] - a[0] * b[2]
cz = a[0] * b[1] - a[1] * b[0]
area = 0.5 * (cx * cx + cy * cy + cz * cz) ** 0.5
total_area += area
i += vertex_count + 1
face_index += 1
return total_area
def calculate_volume(self) -> float:
"""
calculate volume of the mesh if it is closed
"""
if not self.is_closed():
return 0.0
total_volume = 0.0
face_index = 0
i = 0
while i < len(self.faces):
vertex_count = self.faces[i]
if vertex_count >= 3:
face_vertices = self.get_face_vertices(face_index)
v0 = face_vertices[0]
for j in range(1, vertex_count - 1):
v1 = face_vertices[j]
v2 = face_vertices[j + 1]
a = [v0.x, v0.y, v0.z]
b = [v1.x - v0.x, v1.y - v0.y, v1.z - v0.z]
c = [v2.x - v0.x, v2.y - v0.y, v2.z - v0.z]
cx = b[1] * c[2] - b[2] * c[1]
cy = b[2] * c[0] - b[0] * c[2]
cz = b[0] * c[1] - b[1] * c[0]
v = (a[0] * cx + a[1] * cy + a[2] * cz) / 6.0
total_volume += v
i += vertex_count + 1
face_index += 1
return abs(total_volume)
def get_point(self, index: int) -> Point:
"""
get vertex at index as a Point object
"""
if index < 0 or index >= self.vertices_count:
raise IndexError(f"Vertex index {index} out of range")
index *= 3
return Point(
x=self.vertices[index],
y=self.vertices[index + 1],
z=self.vertices[index + 2],
units=self.units,
)
def get_points(self) -> List[Point]:
"""
get all vertices as Point objects
"""
return [self.get_point(i) for i in range(self.vertices_count)]
def get_texture_coordinate(self, index: int) -> Tuple[float, float]:
"""
get texture coordinate at index
"""
if index < 0 or index >= self.texture_coordinates_count:
raise IndexError(f"Texture coordinate index {index} out of range")
index *= 2
return (self.textureCoordinates[index], self.textureCoordinates[index + 1])
def get_face_vertices(self, face_index: int) -> List[Point]:
"""
get the vertices of a specific face
"""
i = 0
current_face = 0
while i < len(self.faces):
if current_face == face_index:
vertex_count = self.faces[i]
vertices = []
for j in range(vertex_count):
vertex_index = self.faces[i + j + 1]
if vertex_index >= self.vertices_count:
raise IndexError(
f"Vertex index {vertex_index} out of range")
vertices.append(self.get_point(vertex_index))
return vertices
vertex_count = self.faces[i]
i += vertex_count + 1
current_face += 1
raise IndexError(f"Face index {face_index} out of range")
def is_closed(self) -> bool:
"""
check if the mesh is closed (verifying each edge appears twice)
"""
edge_counts = {}
i = 0
while i < len(self.faces):
vertex_count = self.faces[i]
for j in range(vertex_count):
v1 = self.faces[i + 1 + j]
v2 = self.faces[i + 1 + ((j + 1) % vertex_count)]
edge = tuple(sorted([v1, v2]))
edge_counts[edge] = edge_counts.get(edge, 0) + 1
i += vertex_count + 1
return all(count == 2 for count in edge_counts.values())
+28
View File
@@ -0,0 +1,28 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.geometry.vector import Vector
from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class Plane(Base, IHasUnits, speckle_type="Objects.Geometry.Plane"):
"""
a plane consisting of an origin Point, and 3 Vectors as its X, Y and Z axis.
"""
origin: Point
normal: Vector
xdir: Vector
ydir: Vector
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}("
f"origin: {self.origin}, "
f"normal: {self.normal}, "
f"xdir: {self.xdir}, "
f"ydir: {self.ydir}, "
f"units: {self.units})"
)
+33
View File
@@ -0,0 +1,33 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class Point(Base, IHasUnits, speckle_type="Objects.Geometry.Point"):
"""
a 3-dimensional point
"""
x: float
y: float
z: float
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, units: {self.units})"
def distance_to(self, other: "Point") -> float:
"""
calculates the distance between this point and another given point.
"""
if not isinstance(other, Point):
raise TypeError(f"Expected Point object, got {type(other)}")
# we assume that host application units are the same for both points
# unit conversion could be expensive, so we avoid it here
dx = other.x - self.x
dy = other.y - self.y
dz = other.z - self.z
return (dx * dx + dy * dy + dz * dz) ** 0.5
@@ -0,0 +1,76 @@
from dataclasses import dataclass
from typing import List
from specklepy.objects.base import Base
from specklepy.objects.geometry.point import Point
from specklepy.objects.interfaces import ICurve, IHasUnits
@dataclass(kw_only=True)
class Polyline(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Polyline"):
"""
a polyline curve, defined by a set of vertices.
"""
value: List[float]
def __repr__(self) -> str:
return f"{self.__class__.__name__}(value: {self.value}, units: {self.units})"
def is_closed(self, tolerance: float = 1e-6) -> bool:
"""
check if the polyline is closed (start point equals end point within tolerance)
"""
if len(self.value) < 6: # need at least 2 points to be closed
return False
# compare first and last points
start = Point(
x=self.value[0],
y=self.value[1],
z=self.value[2],
units=self.units
)
end = Point(
x=self.value[-3],
y=self.value[-2],
z=self.value[-1],
units=self.units
)
return start.distance_to(end) <= tolerance
@property
def length(self) -> float:
return self.__dict__.get('_length', 0.0)
@length.setter
def length(self, value: float) -> None:
self.__dict__['_length'] = value
def calculate_length(self) -> float:
points = self.get_points()
total_length = 0.0
for i in range(len(points) - 1):
total_length += points[i].distance_to(points[i + 1])
if self.is_closed() and points:
total_length += points[-1].distance_to(points[0])
return total_length
def get_points(self) -> List[Point]:
"""
converts the raw coordinate list into Point objects
"""
if len(self.value) % 3 != 0:
raise ValueError(
"Polyline value list is malformed: expected length to be multiple of 3"
)
points = []
for i in range(0, len(self.value), 3):
point = Point(
x=self.value[i],
y=self.value[i + 1],
z=self.value[i + 2],
units=self.units
)
points.append(point)
return points
+22
View File
@@ -0,0 +1,22 @@
from dataclasses import dataclass
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class Vector(Base, IHasUnits, speckle_type="Objects.Geometry.Vector", serialize_ignore = {"length"}):
"""
a 3-dimensional vector
"""
x: float
y: float
z: float
def __repr__(self) -> str:
return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, units: {self.units})"
@property
def length(self) -> float:
return (self.x ** 2 + self.y ** 2 + self.z ** 2) ** 0.5
@@ -58,9 +58,7 @@ class CommitObjectBuilder(ABC, Generic[T]):
if parent_id == ROOT:
parent = root_commit_object
else:
parent = (
self.converted[parent_id] if parent_id in self.converted else None
)
parent = self.converted.get(parent_id, None)
if not parent:
continue
@@ -74,13 +72,15 @@ class CommitObjectBuilder(ABC, Generic[T]):
elements.append(current)
return
except Exception as ex:
# A parent was found, but it was invalid (Likely because of a type mismatch on a `elements` property)
# A parent was found, but it was invalid
# (Likely because of a type mismatch on a `elements` property)
print(
f"Failed to add object {type(current)} to a converted parent; {ex}"
)
raise Exception(
f"Could not find a valid parent for object of type {type(current)}. Checked {len(parents)} potential parent, and non were converted!"
f"Could not find a valid parent for object of type {type(current)}."
f"Checked {len(parents)} potential parent, and non were converted!"
)
@@ -0,0 +1,29 @@
from specklepy.objects.base import Base
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
def has_display_value(x: Base):
return any(hasattr(x, alias) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def create_default_traversal_function() -> GraphTraversal:
"""
Traversal func for traversing the root object of a Speckle Model
"""
convertible_rule = TraversalRule(
[lambda b: b.speckle_type != "Base", has_display_value],
lambda _: ELEMENTS_PROPERTY_ALIASES,
)
default_rule = TraversalRule(
[lambda _: True],
# NOTE: Unlike the C# implementation, this does not ignore Obsolete members
lambda o: o.get_member_names(),
False,
)
return GraphTraversal([convertible_rule, default_rule])
@@ -7,6 +7,10 @@ from specklepy.objects.base import Base
class ITraversalRule(Protocol):
@property
def should_return(self) -> bool:
pass
def get_members_to_traverse(self, o: Base) -> Set[str]:
"""Get the members to traverse."""
pass
@@ -50,20 +54,27 @@ class GraphTraversal:
while len(stack) > 0:
head = stack.pop()
yield head
current = head.current
active_rule = self._get_active_rule_or_default_rule(current)
if active_rule.should_return:
yield head
members_to_traverse = active_rule.get_members_to_traverse(current)
for child_prop in members_to_traverse:
try:
if child_prop in {"speckle_type", "units", "applicationId"}:
continue # debug: to avoid noisy exceptions, explicitly avoid checking ones we know will fail, this is not exhaustive
# debug: to avoid noisy exceptions,
# explicitly avoid checking ones we know will fail,
# this is not exhaustive
continue
if getattr(current, child_prop, None):
value = current[child_prop]
self._traverse_member_to_stack(stack, value, child_prop, head)
except KeyError:
# Unset application ids, and class variables like SpeckleType will throw when __getitem__ is called
# Unset application ids, and class variables like SpeckleType will
# throw when __getitem__ is called
pass
@staticmethod
@@ -114,12 +125,14 @@ class GraphTraversal:
class TraversalRule:
_conditions: Collection[Callable[[Base], bool]]
_members_to_traverse: Callable[[Base], Iterable[str]]
_should_return_to_output: bool = True
@property
def should_return(self) -> bool:
return self._should_return_to_output
def get_members_to_traverse(self, o: Base) -> Set[str]:
return set(self._members_to_traverse(o))
def does_rule_hold(self, o: Base) -> bool:
for condition in self._conditions:
if condition(o):
return True
return False
return any(condition(o) for condition in self._conditions)
+122
View File
@@ -0,0 +1,122 @@
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass, field
from typing import Dict, Generic, List, TypeVar
from specklepy.logging.exceptions import SpeckleInvalidUnitException
from specklepy.objects.base import Base
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
T = TypeVar("T") # define type variable for generic type
# generic interfaces
@dataclass(kw_only=True)
class ICurve(metaclass=ABCMeta):
_domain: Interval = field(default_factory=Interval.unit_interval, init=False)
@property
@abstractmethod
def length(self) -> float:
pass
@property
def domain(self) -> Interval:
return self._domain
@domain.setter
def domain(self, value: Interval) -> None:
if not isinstance(value, Interval):
raise TypeError(f"Domain must be an Interval, got {type(value)}")
self._domain = value
class IDisplayValue(Generic[T], metaclass=ABCMeta):
@property
@abstractmethod
def displayValue(self) -> T:
pass
# field interfaces
@dataclass(kw_only=True)
class IHasUnits(metaclass=ABCMeta):
units: str | Units
_units: str = field(repr=False, init=False)
@property
def units(self) -> str:
return self._units
@units.setter
def units(self, value: str | Units):
if isinstance(value, str):
self._units = value
elif isinstance(value, Units):
self._units = value.value
else:
raise SpeckleInvalidUnitException(
f"Unknown type {type(value)} received for units"
)
@dataclass(kw_only=True)
class IHasArea(metaclass=ABCMeta):
_area: float = field(init=False, repr=False)
@property
def area(self) -> float:
return self._area
@area.setter
def area(self, value: float):
if not isinstance(value, (int, float)):
raise ValueError(f"Area must be a number, got {type(value)}")
self._area = float(value)
@dataclass(kw_only=True)
class IHasVolume(metaclass=ABCMeta):
_volume: float = field(init=False, repr=False)
@property
def volume(self) -> float:
return self._volume
@volume.setter
def volume(self, value: float):
if not isinstance(value, (int, float)):
raise ValueError(f"Volume must be a number, got {type(value)}")
self._volume = float(value)
# data object interfaces
class IProperties(metaclass=ABCMeta):
@property
@abstractmethod
def properties(self) -> Dict[str, object]:
pass
class IDataObject(IProperties, IDisplayValue[List[Base]], metaclass=ABCMeta):
@property
@abstractmethod
def name(self) -> str:
pass
class IBlenderObject(IDataObject, metaclass=ABCMeta):
@property
@abstractmethod
def type(self) -> str:
pass
class IGisObject(IDataObject, metaclass=ABCMeta):
@property
@abstractmethod
def type(self) -> str:
pass
@@ -0,0 +1,37 @@
from dataclasses import dataclass, field
from typing import List
from specklepy.objects.base import Base
@dataclass(kw_only=True)
class Collection(
Base,
# TODO: add deprecated speckle_types
speckle_type="Speckle.Core.Models.Collections.Collection",
detachable={"elements"},
):
"""
A simple container for organising objects within a model
and preserving object hierarchy.
A container is defined by a human-readable name a unique applicationId and
its list of contained elements.
The elements can include an unrestricted number of Base objects including
additional nested Collections.
Note:
A Collection can be for example a Layer in Rhino/AutoCad,
a collection in Blender, or a Category in Revit.
The location of each collection in the hierarchy of collections in a commit
will be retrieved through commit traversal.
Attributes:
name: The human-readable name of the Collection. This name is not necessarily
unique within a commit. Set the applicationId for a unique identifier.
elements: The elements contained in this Collection.
This may include additional nested Collections
"""
name: str
elements: List[Base] = field(default_factory=list)
@@ -0,0 +1,18 @@
from abc import ABCMeta, abstractmethod
class ISpeckleObject(speckle_type="ISpeckleObjects", metaclass=ABCMeta):
@property
@abstractmethod
def id(self) -> str | None:
pass
@property
@abstractmethod
def application_id(self) -> str | None:
pass
@property
@abstractmethod
def speckle_type(self) -> str:
pass
-321
View File
@@ -1,321 +0,0 @@
from typing import Any, List, Optional
from deprecated import deprecated
from specklepy.objects.geometry import Plane, Point, Polyline, Vector
from .base import Base
OTHER = "Objects.Other."
OTHER_REVIT = OTHER + "Revit."
IDENTITY_TRANSFORM = [
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
1.0,
]
class Material(Base, speckle_type=OTHER + "Material"):
"""Generic class for materials containing generic parameters."""
name: Optional[str] = None
class RevitMaterial(Material, speckle_type="Objects.Other.Revit." + "RevitMaterial"):
materialCategory: Optional[str] = None
materialClass: Optional[str] = None
shininess: Optional[int] = None
smoothness: Optional[int] = None
transparency: Optional[int] = None
parameters: Optional[Base] = None
class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
name: Optional[str] = None
opacity: float = 1
metalness: float = 0
roughness: float = 1
diffuse: int = -2894893 # light gray arbg
emissive: int = -16777216 # black arbg
class MaterialQuantity(Base, speckle_type=OTHER + "MaterialQuantity"):
material: Optional[Material] = None
volume: Optional[float] = None
area: Optional[float] = None
class DisplayStyle(Base, speckle_type=OTHER + "DisplayStyle"):
"""
Minimal display style class.
Developed primarily for display styles in Rhino and AutoCAD.
Rhino object attributes uses OpenNURBS definition for linetypes and lineweights.
"""
name: Optional[str] = None
color: int = -2894893 # light gray arbg
linetype: Optional[str] = None
lineweight: float = 0
class Text(Base, speckle_type=OTHER + "Text"):
"""
Text object to render it on viewer.
"""
plane: Plane
value: str
height: float
rotation: float
displayValue: Optional[List[Polyline]] = None
richText: Optional[str] = None
class Transform(
Base,
speckle_type=OTHER + "Transform",
serialize_ignore={"translation", "scaling", "is_identity", "value"},
):
"""The 4x4 transformation matrix
The 3x3 sub-matrix determines scaling.
The 4th column defines translation,
where the last value is a divisor (usually equal to 1).
"""
_value: Optional[List[float]] = None
@property
@deprecated(version="2.12", reason="Use matrix")
def value(self) -> List[float]:
return self._value
@value.setter
def value(self, value: List[float]) -> None:
self.matrix = value
@property
def matrix(self) -> List[float]:
"""The transform matrix represented as a flat list of 16 floats"""
return self._value
@matrix.setter
def matrix(self, value: List[float]) -> None:
try:
value = [float(x) for x in value]
except (ValueError, TypeError) as error:
raise ValueError(
"Could not create a Transform object with the requested value. Input"
f" must be a 16 element list of numbers. Value provided: {value}"
) from error
if len(value) != 16:
raise ValueError(
"Could not create a Transform object: input list should be 16 floats"
f" long, but was {len(value)} long"
)
self._value = value
@property
def translation(self) -> List[float]:
"""The final column of the matrix which defines the translation"""
return [self._value[i] for i in (3, 7, 11, 15)]
@property
def scaling(self) -> List[float]:
"""The 3x3 scaling sub-matrix"""
return [self._value[i] for i in (0, 1, 2, 4, 5, 6, 8, 9, 10)]
@property
def is_identity(self) -> bool:
return self._value == IDENTITY_TRANSFORM
def apply_to_point(self, point: Point) -> Point:
"""Transform a single speckle Point
Arguments:
point {Point} -- the speckle Point to transform
Returns:
Point -- a new transformed point
"""
coords = self.apply_to_point_value([point.x, point.y, point.z])
return Point(x=coords[0], y=coords[1], z=coords[2], units=point.units)
def apply_to_point_value(self, point_value: List[float]) -> List[float]:
"""Transform a list of three floats representing a point
Arguments:
point_value {List[float]} -- a list of 3 floats
Returns:
List[float] -- the list with the transform applied
"""
transformed = [
point_value[0] * self._value[i]
+ point_value[1] * self._value[i + 1]
+ point_value[2] * self._value[i + 2]
+ self._value[i + 3]
for i in range(0, 15, 4)
]
return [transformed[i] / transformed[3] for i in range(3)]
def apply_to_points(self, points: List[Point]) -> List[Point]:
"""Transform a list of speckle Points
Arguments:
points {List[Point]} -- the list of speckle Points to transform
Returns:
List[Point] -- a new list of transformed points
"""
return [self.apply_to_point(point) for point in points]
def apply_to_points_values(self, points_value: List[float]) -> List[float]:
"""Transform a list of speckle Points
Arguments:
points {List[float]}
-- a flat list of floats representing points to transform
Returns:
List[float] -- a new transformed list
"""
if len(points_value) % 3 != 0:
raise ValueError(
"Cannot apply transform as the points list is malformed: expected"
" length to be multiple of 3"
)
transformed = []
for i in range(0, len(points_value), 3):
transformed.extend(self.apply_to_point_value(points_value[i : i + 3]))
return transformed
def apply_to_vector(self, vector: Vector) -> Vector:
"""Transform a single speckle Vector
Arguments:
point {Vector} -- the speckle Vector to transform
Returns:
Vector -- a new transformed point
"""
coords = self.apply_to_vector_value([vector.x, vector.y, vector.z])
return Vector(x=coords[0], y=coords[1], z=coords[2], units=vector.units)
def apply_to_vector_value(self, vector_value: List[float]) -> List[float]:
"""Transform a list of three floats representing a vector
Arguments:
vector_value {List[float]} -- a list of 3 floats
Returns:
List[float] -- the list with the transform applied
"""
return [
vector_value[0] * self._value[i]
+ vector_value[1] * self._value[i + 1]
+ vector_value[2] * self._value[i + 2]
for i in range(0, 15, 4)
][:3]
@classmethod
def from_list(cls, value: Optional[List[float]] = None) -> "Transform":
"""Returns a Transform object from a list of 16 numbers.
If no value is provided, an identity transform will be returned.
Arguments:
value {List[float]} -- the matrix as a flat list of 16 numbers
(defaults to the identity transform)
Returns:
Transform -- a complete transform object
"""
if not value:
value = IDENTITY_TRANSFORM
return cls(value=value)
class BlockDefinition(
Base, speckle_type=OTHER + "BlockDefinition", detachable={"geometry"}
):
name: Optional[str] = None
basePoint: Optional[Point] = None
geometry: Optional[List[Base]] = None
class Instance(Base, speckle_type=OTHER + "Instance", detachable={"definition"}):
transform: Optional[Transform] = None
definition: Optional[Base] = None
class BlockInstance(
Instance, speckle_type=OTHER + "BlockInstance", serialize_ignore={"blockDefinition"}
):
@property
@deprecated(version="2.13", reason="Use definition")
def blockDefinition(self) -> Optional[BlockDefinition]:
if isinstance(self.definition, BlockDefinition):
return self.definition
return None
@blockDefinition.setter
def blockDefinition(self, value: Optional[BlockDefinition]) -> None:
self.definition = value
class RevitInstance(Instance, speckle_type=OTHER_REVIT + "RevitInstance"):
level: Optional[Base] = None
facingFlipped: bool
handFlipped: bool
parameters: Optional[Base] = None
elementId: Optional[str]
# TODO: prob move this into a built elements module, but just trialling this for now
class RevitParameter(Base, speckle_type="Objects.BuiltElements.Revit.Parameter"):
name: Optional[str] = None
value: Any = None
applicationUnitType: Optional[str] = None # eg UnitType UT_Length
applicationUnit: Optional[str] = None # DisplayUnitType eg DUT_MILLIMITERS
applicationInternalName: Optional[
str
] = None # BuiltInParameterName or GUID for shared parameter
isShared: bool = False
isReadOnly: bool = False
isTypeParameter: bool = False
@deprecated(version="2.20", reason="Namespace changed, collectionType deprecated")
class Collection(
Base, speckle_type="Speckle.Core.Models.Collection", detachable={"elements"}
):
name: Optional[str] = None
collectionType: Optional[str] = None
elements: Optional[List[Base]] = None
class Collection( # noqa: F811
Base,
speckle_type="Speckle.Core.Models.Collections.Collection",
detachable={"elements"},
):
name: Optional[str] = None
elements: Optional[List[Base]]
+12 -16
View File
@@ -1,25 +1,21 @@
from typing import Any, List
from dataclasses import dataclass
from specklepy.objects.base import Base
NAMESPACE = "Objects.Primitive"
class Interval(Base, speckle_type=f"{NAMESPACE}.Interval"):
@dataclass(kw_only=True)
class Interval(Base, speckle_type="Objects.Primitive.Interval", serialize_ignore={"length"}):
start: float = 0.0
end: float = 0.0
def length(self):
return abs(self.start - self.end)
def __repr__(self) -> str:
return f"{self.__class__.__name__}(start: {self.start}, end: {self.end})"
@property
def length(self) -> float:
abs(self.end - self.start)
@classmethod
def from_list(cls, args: List[Any]) -> "Interval":
return cls(start=args[0], end=args[1])
def to_list(self) -> List[Any]:
return [self.start, self.end]
class Interval2d(Base, speckle_type=f"{NAMESPACE}.Interval2d"):
u: Interval
v: Interval
def unit_interval(cls) -> "Interval":
interval = cls(start=0, end=1)
return interval
+48
View File
@@ -0,0 +1,48 @@
from dataclasses import dataclass
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.interfaces import IHasUnits
@dataclass(kw_only=True)
class ColorProxy(
Base,
speckle_type="Models.Proxies.ColorProxy",
detachable={"objects"},
):
objects: List[str]
value: int
name: Optional[str]
@dataclass(kw_only=True)
class GroupProxy(
Base,
speckle_type="Models.Proxies.GroupProxy",
detachable={"objects"},
):
objects: List[str]
name: str
@dataclass(kw_only=True)
class InstanceProxy(
Base,
IHasUnits,
speckle_type="Models.Proxies.InstanceProxy",
):
definition_id: str
transform: List[float]
max_depth: int
@dataclass(kw_only=True)
class InstanceDefinitionProxy(
Base,
speckle_type="Models.Proxies.InstanceDefinitionProxy",
detachable={"objects"},
):
objects: List[str]
max_depth: int
name: str
@@ -1,142 +0,0 @@
"""Builtin Speckle object kit."""
from specklepy.objects.structural.analysis import (
Model,
ModelInfo,
ModelSettings,
ModelUnits,
)
from specklepy.objects.structural.axis import Axis, AxisType
from specklepy.objects.structural.geometry import (
Element1D,
Element2D,
Element3D,
ElementType1D,
ElementType2D,
ElementType3D,
Node,
Restraint,
)
from specklepy.objects.structural.loading import (
ActionType,
BeamLoadType,
CombinationType,
FaceLoadType,
Load,
LoadAxisType,
LoadBeam,
LoadCase,
LoadCombinations,
LoadDirection,
LoadDirection2D,
LoadFace,
LoadGravity,
LoadNode,
LoadType,
)
from specklepy.objects.structural.materials import (
Concrete,
MaterialType,
Steel,
StructuralMaterial,
Timber,
)
from specklepy.objects.structural.properties import (
BaseReferencePoint,
MemberType,
Property,
Property1D,
Property2D,
Property3D,
PropertyDamper,
PropertyMass,
PropertySpring,
PropertyType2D,
PropertyType3D,
PropertyTypeDamper,
PropertyTypeSpring,
ReferenceSurface,
ReferenceSurfaceEnum,
SectionProfile,
ShapeType,
shapeType,
)
from specklepy.objects.structural.results import (
Result,
Result1D,
Result2D,
Result3D,
ResultGlobal,
ResultNode,
ResultSet1D,
ResultSet2D,
ResultSet3D,
ResultSetAll,
ResultSetNode,
)
__all__ = [
"Element1D",
"Element2D",
"Element3D",
"ElementType1D",
"ElementType2D",
"ElementType3D",
"AxisType",
"Axis",
"Node",
"Restraint",
"Load",
"LoadType",
"ActionType",
"BeamLoadType",
"FaceLoadType",
"LoadDirection",
"LoadDirection2D",
"LoadAxisType",
"CombinationType",
"LoadBeam",
"LoadCase",
"LoadCombinations",
"LoadFace",
"LoadGravity",
"LoadNode",
"Model",
"ModelInfo",
"ModelSettings",
"ModelUnits",
"MaterialType",
"Concrete",
"StructuralMaterial",
"Steel",
"Timber",
"Property",
"Property1D",
"Property2D",
"Property3D",
"PropertyDamper",
"PropertyMass",
"PropertySpring",
"SectionProfile",
"MemberType",
"BaseReferencePoint",
"ReferenceSurface",
"PropertyType2D",
"PropertyType3D",
"ShapeType",
"PropertyTypeSpring",
"PropertyTypeDamper",
"ReferenceSurfaceEnum",
"shapeType",
"Result",
"Result1D",
"ResultSet1D",
"Result2D",
"ResultSet2D",
"Result3D",
"ResultSet3D",
"ResultGlobal",
"ResultSetNode",
"ResultNode",
"ResultSetAll",
]
@@ -1,49 +0,0 @@
from typing import List, Optional
from specklepy.objects.base import Base
STRUCTURAL_ANALYSIS = "Objects.Structural.Analysis."
class ModelUnits(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelUnits"):
length: Optional[str] = None
sections: Optional[str] = None
displacements: Optional[str] = None
stress: Optional[str] = None
force: Optional[str] = None
mass: Optional[str] = None
time: Optional[str] = None
temperature: Optional[str] = None
velocity: Optional[str] = None
acceleration: Optional[str] = None
energy: Optional[str] = None
angle: Optional[str] = None
strain: Optional[str] = None
class ModelSettings(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelSettings"):
modelUnits: Optional[ModelUnits] = None
steelCode: Optional[str] = None
concreteCode: Optional[str] = None
coincidenceTolerance: float = 0.0
class ModelInfo(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelInfo"):
name: Optional[str] = None
description: Optional[str] = None
projectNumber: Optional[str] = None
projectName: Optional[str] = None
settings: Optional[ModelSettings] = None
initials: Optional[str] = None
application: Optional[str] = None
class Model(Base, speckle_type=STRUCTURAL_ANALYSIS + "Model"):
specs: Optional[ModelInfo] = None
nodes: Optional[List] = None
elements: Optional[List] = None
loads: Optional[List] = None
restraints: Optional[List] = None
properties: Optional[List] = None
materials: Optional[List] = None
layerDescription: Optional[str] = None
-17
View File
@@ -1,17 +0,0 @@
from enum import Enum
from typing import Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry import Plane
class AxisType(int, Enum):
Cartesian = 0
Cylindrical = 1
Spherical = 2
class Axis(Base, speckle_type="Objects.Structural.Geometry.Axis"):
name: Optional[str] = None
axisType: Optional[AxisType] = None
plane: Optional[Plane] = None
@@ -1,110 +0,0 @@
from enum import Enum
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry import Line, Mesh, Plane, Point, Vector
from specklepy.objects.structural.axis import Axis
from specklepy.objects.structural.properties import (
Property1D,
Property2D,
Property3D,
PropertyDamper,
PropertyMass,
PropertySpring,
)
STRUCTURAL_GEOMETRY = "Objects.Structural.Geometry"
class ElementType1D(int, Enum):
Beam = 0
Brace = 1
Bar = 2
Column = 3
Rod = 4
Spring = 5
Tie = 6
Strut = 7
Link = 8
Damper = 9
Cable = 10
Spacer = 11
Other = 12
Null = 13
class ElementType2D(int, Enum):
Quad4 = 0
Quad8 = 1
Triangle3 = 2
Triangle6 = 3
class ElementType3D(int, Enum):
Brick8 = 0
Wedge6 = 1
Pyramid5 = 2
Tetra4 = 3
class Restraint(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Restraint"):
code: Optional[str] = None
stiffnessX: float = 0.0
stiffnessY: float = 0.0
stiffnessZ: float = 0.0
stiffnessXX: float = 0.0
stiffnessYY: float = 0.0
stiffnessZZ: float = 0.0
class Node(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Node"):
name: Optional[str] = None
basePoint: Optional[Point] = None
constraintAxis: Optional[Axis] = None
restraint: Optional[Restraint] = None
springProperty: Optional[PropertySpring] = None
massProperty: Optional[PropertyMass] = None
damperProperty: Optional[PropertyDamper] = None
class Element1D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element1D"):
name: Optional[str] = None
baseLine: Optional[Line] = None
property: Optional[Property1D] = None
type: Optional[ElementType1D] = None
end1Releases: Optional[Restraint] = None
end2Releases: Optional[Restraint] = None
end1Offset: Optional[Vector] = None
end2Offset: Optional[Vector] = None
orientationNode: Optional[Node] = None
orinetationAngle: float = 0.0
localAxis: Optional[Plane] = None
parent: Optional[Base] = None
end1Node: Optional[Node] = None
end2Node: Optional[Node] = None
topology: Optional[List] = None
displayMesh: Optional[Mesh] = None
class Element2D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element2D"):
name: Optional[str] = None
property: Optional[Property2D] = None
type: Optional[ElementType2D] = None
offset: float = 0.0
orientationAngle: float = 0.0
parent: Optional[Base] = None
topology: Optional[List] = None
displayMesh: Optional[Mesh] = None
class Element3D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element3D"):
name: Optional[str] = None
baseMesh: Optional[Mesh] = None
property: Optional[Property3D] = None
type: Optional[ElementType3D] = None
orientationAngle: float = 0.0
parent: Optional[Base] = None
topology: List
# class Storey needs ependency on built elements first
-137
View File
@@ -1,137 +0,0 @@
from enum import Enum
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.geometry import Vector
from specklepy.objects.structural.axis import Axis
STRUCTURAL_LOADING = "Objects.Structural.Loading."
class LoadType(int, Enum):
none = 0
Dead = 1
SuperDead = 2
Soil = 3
Live = 4
LiveRoof = 5
ReducibleLive = 6
Wind = 7
Snow = 8
Rain = 9
Thermal = 10
Notional = 11
Prestress = 12
Equivalent = 13
Accidental = 14
SeismicRSA = 15
SeismicAccTorsion = 16
SeismicStatic = 17
Other = 18
class ActionType(int, Enum):
none = 0
Permanent = 1
Variable = 2
Accidental = 3
class BeamLoadType(int, Enum):
Point = 0
Uniform = 1
Linear = 2
Patch = 3
TriLinear = 4
class FaceLoadType(int, Enum):
Constant = 0
Variable = 1
Point = 2
class LoadDirection2D(int, Enum):
X = 0
Y = 1
Z = 2
class LoadDirection(int, Enum):
X = 0
Y = 1
Z = 2
XX = 3
YY = 4
ZZ = 5
class LoadAxisType(int, Enum):
Global = 0
Local = 1 # local element axes
DeformedLocal = (
2 # element local axis that is embedded in the element as it deforms
)
class CombinationType(int, Enum):
LinearAdd = 0
Envelope = 1
AbsoluteAdd = 2
SRSS = 3
RangeAdd = 4
class LoadCase(Base, speckle_type=STRUCTURAL_LOADING + "LoadCase"):
name: Optional[str] = None
loadType: Optional[LoadType] = None
group: Optional[str] = None
actionType: Optional[ActionType] = None
description: Optional[str] = None
class Load(Base, speckle_type=STRUCTURAL_LOADING + "Load"):
name: Optional[str] = None
loadCase: Optional[LoadCase] = None
class LoadBeam(Load, speckle_type=STRUCTURAL_LOADING + "LoadBeam"):
elements: Optional[List] = None
loadType: Optional[BeamLoadType] = None
direction: Optional[LoadDirection] = None
loadAxis: Optional[Axis] = None
loadAxisType: Optional[LoadAxisType] = None
isProjected: Optional[bool] = None
values: Optional[List] = None
positions: Optional[List] = None
class LoadCombinations(Base, speckle_type=STRUCTURAL_LOADING + "LoadCombination"):
name: Optional[str] = None
loadCases: List
loadFactors: List
combinationType: CombinationType
class LoadFace(Load, speckle_type=STRUCTURAL_LOADING + "LoadFace"):
elements: Optional[List] = None
loadType: Optional[FaceLoadType] = None
direction: Optional[LoadDirection2D] = None
loadAxis: Optional[Axis] = None
loadAxisType: Optional[LoadAxisType] = None
isProjected: Optional[bool] = None
values: Optional[List] = None
positions: Optional[List] = None
class LoadGravity(Load, speckle_type=STRUCTURAL_LOADING + "LoadGravity"):
elements: Optional[List] = None
nodes: Optional[List] = None
gravityFactors: Optional[Vector] = None
class LoadNode(Load, speckle_type=STRUCTURAL_LOADING + "LoadNode"):
nodes: Optional[List] = None
loadAxis: Optional[Axis] = None
direction: Optional[LoadDirection] = None
value: float = 0.0
@@ -1,61 +0,0 @@
from enum import Enum
from typing import Optional
from specklepy.objects.base import Base
STRUCTURAL_MATERIALS = "Objects.Structural.Materials"
class MaterialType(int, Enum):
Concrete = 0
Steel = 1
Timber = 2
Aluminium = 3
Masonry = 4
FRP = 5
Glass = 6
Fabric = 7
Rebar = 8
Tendon = 9
ColdFormed = 10
Other = 11
class StructuralMaterial(
Base, speckle_type=STRUCTURAL_MATERIALS + ".StructuralMaterial"
):
name: Optional[str] = None
grade: Optional[str] = None
materialType: Optional[MaterialType] = None
designCode: Optional[str] = None
codeYear: Optional[str] = None
strength: float = 0.0
elasticModulus: float = 0.0
poissonsRatio: float = 0.0
shearModulus: float = 0.0
density: float = 0.0
thermalExpansivity: float = 0.0
dampingRatio: float = 0.0
cost: float = 0.0
materialSafetyFactor: float = 0.0
class Concrete(StructuralMaterial):
compressiveStrength: float = 0.0
tensileStrength: float = 0.0
flexuralStrength: float = 0.0
maxCompressiveStrain: float = 0.0
maxTensileStrain: float = 0.0
maxAggregateSize: float = 0.0
lightweight: Optional[bool] = None
class Steel(StructuralMaterial, speckle_type=STRUCTURAL_MATERIALS + ".Steel"):
yieldStrength: float = 0.0
ultimateStrength: float = 0.0
maxStrain: float = 0.0
strainHardeningModulus: float = 0.0
class Timber(StructuralMaterial, speckle_type=STRUCTURAL_MATERIALS + ".Timber"):
species: Optional[str] = None
@@ -1,212 +0,0 @@
from enum import Enum
from typing import Optional
from specklepy.objects.base import Base
from specklepy.objects.structural.axis import Axis
from specklepy.objects.structural.materials import StructuralMaterial
STRUCTURAL_PROPERTY = "Objects.Structural.Properties"
class MemberType(int, Enum):
Beam = 0
Column = 1
Generic1D = 2
Slab = 3
Wall = 4
Generic2D = 5
VoidCutter1D = 6
VoidCutter2D = 7
class BaseReferencePoint(int, Enum):
Centroid = 0
TopLeft = 1
TopCentre = 2
TopRight = 3
MidLeft = 4
MidRight = 5
BotLeft = 6
BotCentre = 7
BotRight = 8
class ReferenceSurface(int, Enum):
Top = 0
Middle = 1
Bottom = 2
class PropertyType2D(int, Enum):
Stress = 0
Fabric = 1
Plate = 2
Shell = 3
Curved = 4
Wall = 5
Strain = 6
Axi = 7
Load = 8
class PropertyType3D(int, Enum):
Solid = 0
Infinite = 1
class ShapeType(int, Enum):
Rectangular = 0
Circular = 1
I = 2 # noqa: E741
Tee = 3
Angle = 4
Channel = 5
Perimeter = 6
Box = 7
Catalogue = 8
Explicit = 9
Undefined = 10
class PropertyTypeSpring(int, Enum):
Axial = 0
Torsional = 1
General = 2
Matrix = 3
TensionOnly = 4
CompressionOnly = 5
Connector = 6
LockUp = 7
Gap = 8
Friction = 9
class PropertyTypeDamper(int, Enum):
Axial = 0
Torsional = 1
General = 2
class Property(Base, speckle_type=STRUCTURAL_PROPERTY):
name: Optional[str] = None
class SectionProfile(
Base, speckle_type=STRUCTURAL_PROPERTY + ".Profiles.SectionProfile"
):
name: Optional[str] = None
shapeType: Optional[ShapeType] = None
area: float = 0.0
Iyy: float = 0.0
Izz: float = 0.0
J: float = 0.0
Ky: float = 0.0
weight: float = 0.0
class Property1D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property1D"):
memberType: Optional[MemberType] = None
material: Optional[StructuralMaterial] = None
profile: Optional[SectionProfile] = None
referencePoint: Optional[BaseReferencePoint] = None
offsetY: float = 0.0
offsetZ: float = 0.0
class Property2D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property2D"):
type: Optional[PropertyType2D] = None
thickness: float = 0.0
material: Optional[StructuralMaterial] = None
orientationAxis: Optional[Axis] = None
refSurface: Optional[ReferenceSurface] = None
zOffset: float = 0.0
modifierInPlane: float = 0.0
modifierBending: float = 0.0
modifierShear: float = 0.0
modifierVolume: float = 0.0
class Property3D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property3D"):
type: Optional[PropertyType3D] = None
material: Optional[StructuralMaterial] = None
orientationAxis: Optional[Axis] = None
class PropertyDamper(Property, speckle_type=STRUCTURAL_PROPERTY + ".PropertyDamper"):
damperType: Optional[PropertyTypeDamper] = None
dampingX: float = 0.0
dampingY: float = 0.0
dampingZ: float = 0.0
dampingXX: float = 0.0
dampingYY: float = 0.0
dampingZZ: float = 0.0
class PropertyMass(Property, speckle_type=STRUCTURAL_PROPERTY + ".PropertyMass"):
mass: float = 0.0
inertiaXX: float = 0.0
inertiaYY: float = 0.0
inertiaZZ: float = 0.0
inertiaXY: float = 0.0
inertiaYZ: float = 0.0
inertiaZX: float = 0.0
massModified: Optional[bool] = None
massModifierX: float = 0.0
massModifierY: float = 0.0
massModifierZ: float = 0.0
class PropertySpring(Property, speckle_type=STRUCTURAL_PROPERTY + ".PropertySpring"):
springType: Optional[PropertyTypeSpring] = None
springCurveX: float = 0.0
stiffnessX: float = 0.0
springCurveY: float = 0.0
stiffnessY: float = 0.0
springCurveZ: float = 0.0
stiffnessZ: float = 0.0
springCurveXX: float = 0.0
stiffnessXX: float = 0.0
springCurveYY: float = 0.0
stiffnessYY: float = 0.0
springCurveZZ: float = 0.0
stiffnessZZ: float = 0.0
dampingRatio: float = 0.0
dampingX: float = 0.0
dampingY: float = 0.0
dampingZ: float = 0.0
dampingXX: float = 0.0
dampingYY: float = 0.0
dampingZZ: float = 0.0
matrix: float = 0.0
postiveLockup: float = 0.0
frictionCoefficient: float = 0.0
class ReferenceSurfaceEnum(int, Enum):
Concrete = 0
Steel = 1
Timber = 2
Aluminium = 3
Masonry = 4
FRP = 5
Glass = 6
Fabric = 7
Rebar = 8
Tendon = 9
ColdFormed = 10
Other = 11
class shapeType(int, Enum):
Concrete = 0
Steel = 1
Timber = 2
Aluminium = 3
Masonry = 4
FRP = 5
Glass = 6
Fabric = 7
Rebar = 8
Tendon = 9
ColdFormed = 10
Other = 11
-172
View File
@@ -1,172 +0,0 @@
from typing import List, Optional
from specklepy.objects.base import Base
from specklepy.objects.structural.analysis import Model
from specklepy.objects.structural.geometry import Element1D, Element2D, Element3D, Node
STRUCTURAL_RESULTS = "Objects.Structural.Results."
class Result(Base, speckle_type=STRUCTURAL_RESULTS + "Result"):
resultCase: Optional[Base] = None
permutation: Optional[str] = None
description: Optional[str] = None
class ResultSet1D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet1D"):
results1D: List
class Result1D(Result, speckle_type=STRUCTURAL_RESULTS + "Result1D"):
element: Optional[Element1D] = None
position: Optional[float] = None
dispX: Optional[float] = None
dispY: Optional[float] = None
dispZ: Optional[float] = None
rotXX: Optional[float] = None
rotYY: Optional[float] = None
rotZZ: Optional[float] = None
forceX: Optional[float] = None
forceY: Optional[float] = None
forceZ: Optional[float] = None
momentXX: Optional[float] = None
momentYY: Optional[float] = None
momentZZ: Optional[float] = None
axialStress: Optional[float] = None
shearStressY: Optional[float] = None
shearStressZ: Optional[float] = None
bendingStressYPos: Optional[float] = None
bendingStressYNeg: Optional[float] = None
bendingStressZPos: Optional[float] = None
bendingStressZNeg: Optional[float] = None
combinedStressMax: Optional[float] = None
combinedStressMin: Optional[float] = None
class ResultSet2D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet2D"):
results2D: List
class Result2D(Result, speckle_type=STRUCTURAL_RESULTS + "Result2D"):
element: Optional[Element2D] = None
position: List
dispX: Optional[float] = None
dispY: Optional[float] = None
dispZ: Optional[float] = None
forceXX: Optional[float] = None
forceYY: Optional[float] = None
forceXY: Optional[float] = None
momentXX: Optional[float] = None
momentYY: Optional[float] = None
momentXY: Optional[float] = None
shearX: Optional[float] = None
shearY: Optional[float] = None
stressTopXX: Optional[float] = None
stressTopYY: Optional[float] = None
stressTopZZ: Optional[float] = None
stressTopXY: Optional[float] = None
stressTopYZ: Optional[float] = None
stressTopZX: Optional[float] = None
stressMidXX: Optional[float] = None
stressMidYY: Optional[float] = None
stressMidZZ: Optional[float] = None
stressMidXY: Optional[float] = None
stressMidYZ: Optional[float] = None
stressMidZX: Optional[float] = None
stressBotXX: Optional[float] = None
stressBotYY: Optional[float] = None
stressBotZZ: Optional[float] = None
stressBotXY: Optional[float] = None
stressBotYZ: Optional[float] = None
stressBotZX: Optional[float] = None
class ResultSet3D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet3D"):
results3D: List
class Result3D(Result, speckle_type=STRUCTURAL_RESULTS + "Result3D"):
element: Optional[Element3D] = None
position: List
dispX: Optional[float] = None
dispY: Optional[float] = None
dispZ: Optional[float] = None
stressXX: Optional[float] = None
stressYY: Optional[float] = None
stressZZ: Optional[float] = None
stressXY: Optional[float] = None
stressYZ: Optional[float] = None
stressZX: Optional[float] = None
class ResultGlobal(Result, speckle_type=STRUCTURAL_RESULTS + "ResultGlobal"):
model: Optional[Model] = None
loadX: Optional[float] = None
loadY: Optional[float] = None
loadZ: Optional[float] = None
loadXX: Optional[float] = None
loadYY: Optional[float] = None
loadZZ: Optional[float] = None
reactionX: Optional[float] = None
reactionY: Optional[float] = None
reactionZ: Optional[float] = None
reactionXX: Optional[float] = None
reactionYY: Optional[float] = None
reactionZZ: Optional[float] = None
mode: Optional[float] = None
frequency: Optional[float] = None
loadFactor: Optional[float] = None
modalStiffness: Optional[float] = None
modalGeoStiffness: Optional[float] = None
effMassX: Optional[float] = None
effMassY: Optional[float] = None
effMassZ: Optional[float] = None
effMassXX: Optional[float] = None
effMassYY: Optional[float] = None
effMassZZ: Optional[float] = None
class ResultSetNode(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSetNode"):
resultsNode: List
class ResultNode(Result, speckle_type=STRUCTURAL_RESULTS + " ResultNode"):
node: Optional[Node] = None
dispX: Optional[float] = None
dispY: Optional[float] = None
dispZ: Optional[float] = None
rotXX: Optional[float] = None
rotYY: Optional[float] = None
rotZZ: Optional[float] = None
reactionX: Optional[float] = None
reactionY: Optional[float] = None
reactionZ: Optional[float] = None
reactionXX: Optional[float] = None
reactionYY: Optional[float] = None
reactionZZ: Optional[float] = None
constraintX: Optional[float] = None
constraintY: Optional[float] = None
constraintZ: Optional[float] = None
constraintXX: Optional[float] = None
constraintYY: Optional[float] = None
constraintZZ: Optional[float] = None
velX: Optional[float] = None
velY: Optional[float] = None
velZ: Optional[float] = None
velXX: Optional[float] = None
velYY: Optional[float] = None
velZZ: Optional[float] = None
accX: Optional[float] = None
accY: Optional[float] = None
accZ: Optional[float] = None
accXX: Optional[float] = None
accYY: Optional[float] = None
accZZ: Optional[float] = None
class ResultSetAll(Base, speckle_type=None):
resultSet1D: Optional[ResultSet1D] = None
resultSet2D: Optional[ResultSet2D] = None
resultSet3D: Optional[ResultSet3D] = None
resultsGlobal: Optional[ResultGlobal] = None
resultsNode: Optional[ResultSetNode] = None
+9
View File
@@ -0,0 +1,9 @@
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)
+159
View File
@@ -0,0 +1,159 @@
import math
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Arc, Plane, Point, Vector
from specklepy.objects.models.units import Units
from specklepy.objects.primitive import Interval
@pytest.fixture
def sample_points():
start = Point(x=1.0, y=0.0, z=0.0, units=Units.m)
mid = Point(x=0.0, y=1.0, z=0.0, units=Units.m)
end = Point(x=-1.0, y=0.0, z=0.0, units=Units.m)
return start, mid, end
@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)
plane = Plane(origin=origin, normal=normal,
xdir=xdir, ydir=ydir, units=Units.m)
return plane
@pytest.fixture
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
)
return arc
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
)
assert arc.startPoint == start
assert arc.midPoint == mid
assert arc.endPoint == end
assert arc.plane == sample_plane
assert arc.units == Units.m.value
def test_arc_domain(sample_arc):
assert isinstance(sample_arc.domain, Interval)
assert sample_arc.domain.start == 0.0
assert sample_arc.domain.end == 1.0
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):
start, mid, end = sample_points
arc = Arc(
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
)
def test_arc_serialization(sample_arc):
serialized = serialize(sample_arc)
deserialized = deserialize(serialized)
assert deserialized.startPoint.x == sample_arc.startPoint.x
assert deserialized.startPoint.y == sample_arc.startPoint.y
assert deserialized.startPoint.z == sample_arc.startPoint.z
assert deserialized.midPoint.x == sample_arc.midPoint.x
assert deserialized.midPoint.y == sample_arc.midPoint.y
assert deserialized.midPoint.z == sample_arc.midPoint.z
assert deserialized.endPoint.x == sample_arc.endPoint.x
assert deserialized.endPoint.y == sample_arc.endPoint.y
assert deserialized.endPoint.z == sample_arc.endPoint.z
assert deserialized.plane.origin.x == sample_arc.plane.origin.x
assert deserialized.plane.origin.y == sample_arc.plane.origin.y
assert deserialized.plane.origin.z == sample_arc.plane.origin.z
assert deserialized.units == sample_arc.units
+86
View File
@@ -0,0 +1,86 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Line, Point
from specklepy.objects.models.units import Units
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
@pytest.fixture
def sample_line(sample_points):
start, end = sample_points
line = Line(start=start, end=end, units=Units.m)
return line
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
def test_line_units(sample_points):
start, end = sample_points
line = Line(start=start, end=end, units=Units.m)
assert line.units == Units.m.value
# Test setting units with string
line.units = "mm"
assert line.units == "mm"
def test_line_serialization(sample_line):
serialized = serialize(sample_line)
deserialized = deserialize(serialized)
assert deserialized.start.x == sample_line.start.x
assert deserialized.start.y == sample_line.start.y
assert deserialized.start.z == sample_line.start.z
assert deserialized.end.x == sample_line.end.x
assert deserialized.end.y == sample_line.end.y
assert deserialized.end.z == sample_line.end.z
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")
+185
View File
@@ -0,0 +1,185 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry.mesh import Mesh
from specklepy.objects.geometry.point import Point
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
]
@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)
]
@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
]
@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
]
@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
)
def test_mesh_creation(cube_vertices, cube_faces):
mesh = Mesh(vertices=cube_vertices, faces=cube_faces, units=Units.m)
assert mesh.vertices == cube_vertices
assert mesh.faces == cube_faces
assert mesh.colors == []
assert mesh.textureCoordinates == []
assert mesh.units == Units.m.value
def test_mesh_vertex_count(sample_mesh):
assert sample_mesh.vertices_count == 8 # cube has 8 vertices
def test_mesh_texture_coordinates_count(full_mesh):
assert full_mesh.texture_coordinates_count == 8 # one UV per vertex
def test_mesh_get_point(sample_mesh):
point = sample_mesh.get_point(0)
assert isinstance(point, Point)
assert point.x == -0.5
assert point.y == -0.5
assert point.z == -0.5
assert point.units == sample_mesh.units
with pytest.raises(IndexError):
sample_mesh.get_point(8) # Beyond vertex count
def test_mesh_get_points(sample_mesh):
points = sample_mesh.get_points()
assert len(points) == 8
assert all(isinstance(p, Point) for p in points)
assert all(p.units == sample_mesh.units for p in points)
def test_mesh_get_texture_coordinate(full_mesh):
uv = full_mesh.get_texture_coordinate(0)
assert uv == (0.0, 0.0)
with pytest.raises(IndexError):
full_mesh.get_texture_coordinate(8) # beyond UV count
def test_mesh_get_face_vertices(sample_mesh):
face_vertices = sample_mesh.get_face_vertices(0)
assert len(face_vertices) == 4
assert all(isinstance(v, Point) for v in face_vertices)
with pytest.raises(IndexError):
sample_mesh.get_face_vertices(6) # beyond face count
def test_mesh_is_closed(sample_mesh):
assert sample_mesh.is_closed() # cube is a closed 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
# Verify volume is set correctly
assert sample_mesh.volume == pytest.approx(1.0)
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
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
mesh = Mesh(vertices=vertices, faces=[3, 0, 1, 5], units=Units.m)
mesh.get_face_vertices(0)
def test_mesh_serialization(full_mesh):
serialized = serialize(full_mesh)
deserialized = deserialize(serialized)
assert deserialized.vertices == full_mesh.vertices
assert deserialized.faces == full_mesh.faces
assert deserialized.colors == full_mesh.colors
assert deserialized.textureCoordinates == full_mesh.textureCoordinates
assert deserialized.units == full_mesh.units
+117
View File
@@ -0,0 +1,117 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Plane, Point, 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
@pytest.fixture
def sample_vectors():
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
@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
)
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
)
assert plane.units == Units.m.value
plane.units = "mm"
assert plane.units == "mm"
def test_plane_invalid_construction():
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)
with pytest.raises(Exception):
Plane(origin="not a point", normal=normal, xdir=xdir, ydir=ydir)
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")
def test_plane_serialization(sample_plane):
serialized = serialize(sample_plane)
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.normal.x == sample_plane.normal.x
assert deserialized.normal.y == sample_plane.normal.y
assert deserialized.normal.z == sample_plane.normal.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.ydir.x == sample_plane.ydir.x
assert deserialized.ydir.y == sample_plane.ydir.y
assert deserialized.ydir.z == sample_plane.ydir.z
assert deserialized.units == sample_plane.units
+36
View File
@@ -0,0 +1,36 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Point
from specklepy.objects.models.units import Units
def test_point_creation():
p1 = Point(x=1.0, y=2.0, z=3.0, units=Units.m)
assert p1.x == 1.0
assert p1.y == 2.0
assert p1.z == 3.0
assert p1.units == Units.m.value
def test_point_distance_calculation():
p1 = Point(x=1.0, y=2.0, z=3.0, units=Units.m)
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)
assert distance == pytest.approx(expected)
with pytest.raises(TypeError):
p1.distance_to("not a point")
def test_point_serialization():
p1 = Point(x=1.0, y=2.0, z=3.0, units=Units.mm)
serialized = serialize(p1)
deserialized = deserialize(serialized)
assert deserialized.x == p1.x
assert deserialized.y == p1.y
assert deserialized.z == p1.z
assert deserialized.units == p1.units
@@ -0,0 +1,123 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Point, Polyline
from specklepy.objects.models.units import Units
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
]
@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)
]
@pytest.fixture
def sample_polyline(open_square_coords):
return Polyline(value=open_square_coords, units=Units.m)
def test_polyline_creation(open_square_coords):
polyline = Polyline(value=open_square_coords, units=Units.m)
assert polyline.value == open_square_coords
assert polyline.units == Units.m.value
def test_polyline_domain(sample_polyline):
assert isinstance(sample_polyline.domain, Interval)
assert sample_polyline.domain.start == 0.0
assert sample_polyline.domain.end == 1.0
def test_polyline_is_closed(open_square_coords, closed_square_coords):
open_poly = Polyline(value=open_square_coords, units=Units.m)
closed_poly = Polyline(value=closed_square_coords, units=Units.m)
assert not open_poly.is_closed()
assert closed_poly.is_closed()
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
poly = Polyline(value=almost_closed, units=Units.m)
assert not poly.is_closed(tolerance=1e-6)
assert poly.is_closed(tolerance=0.01)
def test_polyline_length_open(sample_polyline):
sample_polyline.length = sample_polyline.calculate_length()
assert sample_polyline.length == 3.0
def test_polyline_length_closed(closed_square_coords):
polyline = Polyline(value=closed_square_coords, units=Units.m)
polyline.length = polyline.calculate_length()
assert polyline.length == 4.0
def test_polyline_get_points(sample_polyline):
points = sample_polyline.get_points()
assert len(points) == 4
assert all(isinstance(p, Point) for p in points)
assert all(p.units == Units.m.value for p in points)
# Create expected points
expected_points = [
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)
]
# Check coordinates match
for actual, expected in zip(points, expected_points, strict=False):
assert actual.x == expected.x
assert actual.y == expected.y
assert actual.z == expected.z
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)
polyline.get_points()
def test_polyline_units(open_square_coords):
polyline = Polyline(value=open_square_coords, units=Units.m)
assert polyline.units == Units.m.value
polyline.units = "mm"
assert polyline.units == "mm"
def test_polyline_serialization(sample_polyline):
serialized = serialize(sample_polyline)
deserialized = deserialize(serialized)
assert deserialized.value == sample_polyline.value
assert deserialized.units == sample_polyline.units
assert deserialized.domain.start == sample_polyline.domain.start
assert deserialized.domain.end == sample_polyline.domain.end
@@ -0,0 +1,42 @@
import pytest
from specklepy.core.api.operations import deserialize, serialize
from specklepy.objects.geometry import Vector
from specklepy.objects.models.units import Units
def test_vector_creation():
v = Vector(x=1.0, y=2.0, z=3.0, units=Units.m)
assert v.x == 1.0
assert v.y == 2.0
assert v.z == 3.0
assert v.units == Units.m.value
def test_vector_length():
v = Vector(x=3.0, y=4.0, z=0.0, units=Units.m)
assert v.length == pytest.approx(5.0) # 3-4-5 triangle
def test_vector_units():
v = Vector(x=1.0, y=2.0, z=3.0, units=Units.m)
assert v.units == Units.m.value
v.units = "mm"
assert v.units == "mm"
def test_vector_invalid_construction():
with pytest.raises(TypeError):
Vector(x=1.0, y=2.0) # missing z and units
def test_vector_serialization():
v = Vector(x=1.0, y=2.0, z=3.0, units=Units.m)
serialized = serialize(v)
deserialized = deserialize(serialized)
assert deserialized.x == v.x
assert deserialized.y == v.y
assert deserialized.z == v.z
assert deserialized.units == v.units
@@ -30,6 +30,7 @@ def safe_json_loads(obj: str, obj_id=None) -> Any:
f"Failed to deserialise object (id: {obj_id}). This is likely a ujson big"
f" int error - falling back to json. \nError: {err}",
SpeckleWarning,
stacklevel=2,
)
return json.loads(obj)
@@ -140,7 +141,8 @@ class BaseObjectSerializer:
object_builder[prop] = value
continue
# NOTE: for dynamic props, this won't be re-serialised as an enum but as an int
# NOTE: for dynamic props, this won't be re-serialised
# as an enum but as an int
if isinstance(value, Enum):
object_builder[prop] = value.value
continue
@@ -222,7 +224,7 @@ class BaseObjectSerializer:
if isinstance(obj, Enum):
return obj.value
elif isinstance(obj, (list, tuple, set)):
elif isinstance(obj, list | tuple | set):
if not detach:
return [self.traverse_value(o) for o in obj]
@@ -257,6 +259,7 @@ class BaseObjectSerializer:
f"Failed to handle {type(obj)} in"
" `BaseObjectSerializer.traverse_value`",
SpeckleWarning,
stacklevel=2,
)
return str(obj)
@@ -342,7 +345,11 @@ class BaseObjectSerializer:
object_type = Base.get_registered_type(speckle_type)
# initialise the base object using `speckle_type` fall back to base if needed
base = object_type() if object_type else Base.of_type(speckle_type=speckle_type)
base = (
object_type.__new__(object_type)
if object_type
else Base.of_type(speckle_type=speckle_type)
)
# get total children count
if "__closure" in obj:
if not self.read_transport:
@@ -370,6 +377,7 @@ class BaseObjectSerializer:
f"Could not find the referenced child object of id `{ref_id}`"
f" in the given read transport: {self.read_transport.name}",
SpeckleWarning,
stacklevel=2,
)
base.__setattr__(prop, self.handle_value(value))
@@ -433,6 +441,7 @@ class BaseObjectSerializer:
f"Could not find the referenced child object of id `{ref_id}` in the"
f" given read transport: {self.read_transport.name}",
SpeckleWarning,
stacklevel=2,
)
return obj
+2 -2
View File
@@ -27,8 +27,8 @@ class MemoryTransport(AbstractTransport):
) -> None:
raise NotImplementedError
def get_object(self, id: str) -> str or None:
return self.objects[id] if id in self.objects else None
def get_object(self, id: str) -> str | None:
return self.objects.get(id, None)
def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
return {id: (id in self.objects) for id in id_list}
@@ -11,7 +11,7 @@ from specklepy.logging.exceptions import SpeckleException
LOG = logging.getLogger(__name__)
class BatchSender(object):
class BatchSender:
def __init__(
self,
server_url,
@@ -123,8 +123,14 @@ class BatchSender(object):
upload_data = "[" + ",".join(new_objects) + "]"
upload_data_gzip = gzip.compress(upload_data.encode())
LOG.info(
"Uploading batch of %s objects (%s new): (size: %s, compressed size: %s)"
% (len(batch), len(new_objects), len(upload_data), len(upload_data_gzip))
"Uploading batch of {batch_size} objects {new_object_count}: ",
"(size: {upload_size}, compressed size: {upload_data_size})",
{
"batch_size": len(batch),
"new_object_count": len(new_objects),
"upload_size": len(upload_data),
"upload_data_size": len(upload_data_gzip),
},
)
try:
+2 -1
View File
@@ -74,7 +74,8 @@ class ServerTransport(AbstractTransport):
SpeckleWarning(
"Unauthenticated Speckle Client provided to Server Transport"
f" for {url}. Receiving from private streams will fail."
)
),
stacklevel=2,
)
else:
self.account = client.account
+3 -5
View File
@@ -39,8 +39,7 @@ class SQLiteTransport(AbstractTransport):
f"SQLiteTransport could not initialise {self.scope}.db at"
f" {self._base_path}. Either provide a different `base_path` or use an"
" alternative transport.",
ex,
)
) from ex
def __repr__(self) -> str:
return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')"
@@ -105,10 +104,9 @@ class SQLiteTransport(AbstractTransport):
raise SpeckleException(
"Could not save the batch of objects to the local db. Inner exception:"
f" {ex}",
ex,
)
) from ex
def get_object(self, id: str) -> str or None:
def get_object(self, id: str) -> str | None:
self.__check_connection()
with closing(self.__connection.cursor()) as c:
row = c.execute(
@@ -2,7 +2,7 @@ import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter, UserUpdateInput
from specklepy.core.api.models import ResourceCollection, User
@@ -42,3 +42,22 @@ class TestActiveUserResource:
assert len(res.items) == len(existing.items) + 2
assert any(project.id == p1.id for project in res.items)
assert any(project.id == p2.id for project in res.items)
def test_active_user_get_projects_with_filter(self, client: SpeckleClient):
# Since the client may be reused for other tests,
# this test does rely on no other test creating a project
# with "Search for me" in its name
p1 = client.project.create(
ProjectCreateInput(name="Search for me!", description=None, visibility=None)
)
_ = client.project.create(
ProjectCreateInput(name="But not me!", description=None, visibility=None)
)
filter = UserProjectsFilter(search="Search for me")
res = client.active_user.get_projects(filter=filter)
assert isinstance(res, ResourceCollection)
assert len(res.items) == 1
assert res.totalCount == 1
assert res.items[0].id == p1.id
@@ -6,7 +6,10 @@ from specklepy.core.api.inputs.model_inputs import (
DeleteModelInput,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectModelsFilter,
)
from specklepy.core.api.models.current import (
Model,
Project,
@@ -65,6 +68,18 @@ class TestModelResource:
assert result.createdAt == test_model.createdAt
assert result.updatedAt == test_model.updatedAt
def test_models_get_with_filter(
self, client: SpeckleClient, test_model: Model, test_project: Project
):
filter = ProjectModelsFilter(search=test_model.name)
result = client.model.get_models(test_project.id, models_filter=filter)
assert isinstance(result, ResourceCollection)
assert len(result.items) == 1
assert result.totalCount == 1
assert result.items[0].id == test_model.id
def test_get_models(
self, client: SpeckleClient, test_project: Project, test_model: Model
):
@@ -82,6 +97,20 @@ class TestModelResource:
assert isinstance(result, ProjectWithModels)
assert result.id == test_project.id
assert isinstance(result.models, ResourceCollection)
assert len(result.models.items) == 1
assert result.models.totalCount == 1
assert result.models.items[0].id == test_model.id
def test_project_get_models_with_filter(
self, client: SpeckleClient, test_project: Project, test_model: Model
):
filter = ProjectModelsFilter(search=test_model.name)
result = client.project.get_with_models(test_project.id, models_filter=filter)
assert isinstance(result, ProjectWithModels)
assert result.id == test_project.id
assert isinstance(result.models, ResourceCollection)
assert len(result.models.items) == 1
assert result.models.totalCount == 1
assert result.models.items[0].id == test_model.id
@@ -1,7 +1,7 @@
import pytest
from specklepy.api.client import SpeckleClient
from specklepy.core.api.inputs.model_inputs import CreateModelInput
from specklepy.core.api.inputs.model_inputs import CreateModelInput, ModelVersionsFilter
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
from specklepy.core.api.inputs.version_inputs import (
DeleteVersionsInput,
@@ -76,6 +76,26 @@ class TestVersionResource:
assert result.totalCount == 1
assert result.items[0].id == test_version.id
def test_versions_get_with_filter(
self,
client: SpeckleClient,
test_model_1: Model,
test_project: Project,
test_version: Version,
):
filter = ModelVersionsFilter(
priorityIds=[test_version.id], priorityIdsOnly=True
)
result = client.version.get_versions(
test_model_1.id, test_project.id, filter=filter
)
assert isinstance(result, ResourceCollection)
assert len(result.items) == 1
assert result.totalCount == 1
assert result.items[0].id == test_version.id
def test_version_received(
self, client: SpeckleClient, test_version: Version, test_project: Project
):
@@ -103,9 +123,34 @@ class TestVersionResource:
assert result.versions.totalCount == 1
assert result.versions.items[0].id == test_version.id
def test_version_update(self, client: SpeckleClient, test_version: Version):
def test_model_get_with_versions_with_filter(
self,
client: SpeckleClient,
test_model_1: Model,
test_project: Project,
test_version: Version,
):
filter = ModelVersionsFilter(
priorityIds=[test_version.id], priorityIdsOnly=True
)
result = client.model.get_with_versions(
test_model_1.id, test_project.id, versions_filter=filter
)
assert isinstance(result, ModelWithVersions)
assert len(result.versions.items) == 1
assert result.versions.totalCount == 1
assert isinstance(result.versions, ResourceCollection)
assert result.versions.items[0].id == test_version.id
def test_version_update(
self, client: SpeckleClient, test_version: Version, test_project: Project
):
new_message = "MY new version message"
input = UpdateVersionInput(versionId=test_version.id, message=new_message)
input = UpdateVersionInput(
versionId=test_version.id, projectId=test_project.id, message=new_message
)
updated_version = client.version.update(input)
assert isinstance(updated_version, Version)
@@ -121,7 +166,9 @@ class TestVersionResource:
test_model_2: Model,
):
input = MoveVersionsInput(
targetModelName=test_model_2.name, versionIds=[test_version.id]
targetModelName=test_model_2.name,
versionIds=[test_version.id],
projectId=test_project.id,
)
moved_model_id = client.version.move_to_model(input)
@@ -137,7 +184,9 @@ class TestVersionResource:
def test_version_delete(
self, client: SpeckleClient, test_version: Version, test_project: Project
):
input = DeleteVersionsInput(versionIds=[test_version.id])
input = DeleteVersionsInput(
versionIds=[test_version.id], projectId=test_project.id
)
response = client.version.delete(input)
assert response is True
@@ -1,47 +0,0 @@
import pytest
from specklepy.api.models import Stream
from specklepy.objects import Base
from specklepy.objects.encoding import ObjectArray
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
class TestObject:
@pytest.fixture(scope="module")
def stream(self, client):
stream = Stream(
name="a sample stream for testing",
description="a stream created for testing",
isPublic=True,
)
stream.id = client.stream.create(
stream.name, stream.description, stream.isPublic
)
return stream
def test_object_create(self, client, stream, base):
transport = SQLiteTransport()
s = BaseObjectSerializer(write_transports=[transport], read_transport=transport)
_, base_dict = s.traverse_base(base)
obj_id = client.object.create(stream_id=stream.id, objects=[base_dict])[0]
assert isinstance(obj_id, str)
assert base_dict["@detach"]["speckle_type"] == "reference"
assert obj_id == base.get_id(True)
def test_object_get(self, client, stream, base):
fetched_base = client.object.get(
stream_id=stream.id, object_id=base.get_id(True)
)
assert isinstance(fetched_base, Base)
assert fetched_base.name == base.name
assert isinstance(fetched_base.vertices, list)
# assert fetched_base["@detach"]["speckle_type"] == "reference"
def test_object_array_decoder(self):
array = ObjectArray()
array.data = [5, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 3, 1, 1, 1, 2, 1, 1, 1, 1]
assert array.decode(decoder=sum) == [5, 4, 3, 2, 1]
@@ -33,8 +33,8 @@ class TestOtherUser:
assert isinstance(fetched_user, LimitedUser)
assert fetched_user.name == second_user_dict["name"]
# changed in the server, now you cannot get emails of other users
# not checking this, since the first user could or could not be an admin on the server
# admins can get emails of others, regular users can't
# not checking this, since the first user could or could not be an admin
# on the server, admins can get emails of others, regular users can't
# assert fetched_user.email == None
second_user_dict["id"] = fetched_user.id
@@ -36,8 +36,8 @@ class TestUser:
assert isinstance(fetched_user, User)
assert fetched_user.name == second_user_dict["name"]
# changed in the server, now you cannot get emails of other users
# not checking this, since the first user could or could not be an admin on the server
# admins can get emails of others, regular users can't
# not checking this, since the first user could or could not be an admin
# on the server, admins can get emails of others, regular users can't
# assert fetched_user.email == None
second_user_dict["id"] = fetched_user.id
+5 -3
View File
@@ -12,7 +12,7 @@ from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.core.api.models import Stream, Version
from specklepy.logging import metrics
from specklepy.objects.base import Base
from specklepy.objects.fakemesh import FakeDirection, FakeMesh
from .fakemesh import FakeMesh, FakeDirection
from specklepy.objects.geometry import Point
from specklepy.transports.server.server import ServerTransport
@@ -44,7 +44,8 @@ def seed_user(host: str) -> Dict[str, str]:
if not r.ok:
raise Exception(f"Cannot seed user: {r.reason}")
redirect_url = urlparse(r.headers.get("location"))
access_code = parse_qs(redirect_url.query)["access_code"][0] # type: ignore
access_code = parse_qs(redirect_url.query)[
"access_code"][0] # type: ignore
r_tokens = requests.post(
url=f"http://{host}/auth/token",
@@ -113,7 +114,8 @@ def sample_stream(client: SpeckleClient) -> Stream:
description="a stream created for testing",
isPublic=True,
)
stream.id = client.stream.create(stream.name, stream.description, stream.isPublic)
stream.id = client.stream.create(
stream.name, stream.description, stream.isPublic)
return stream
@@ -3,7 +3,7 @@ from typing import List, Optional
from specklepy.objects.geometry import Point
from .base import Base
from specklepy.objects.base import Base
CHUNKABLE_PROPS = {
"vertices": 100,
@@ -16,7 +16,9 @@ CHUNKABLE_PROPS = {
DETACHABLE = {"detach_this", "origin", "detached_list"}
class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
class FakeGeo(
Base, speckle_type="FakeGeo", chunkable={"dots": 50}, detachable={"pointslist"}
):
pointslist: Optional[List[Base]] = None
dots: Optional[List[int]] = None
@@ -28,7 +30,9 @@ class FakeDirection(Enum):
WEST = 4
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
class FakeMesh(
FakeGeo, speckle_type="FakeMesh", chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE
):
vertices: Optional[List[float]] = None
faces: Optional[List[int]] = None
colors: Optional[List[int]] = None
@@ -1,4 +1,5 @@
"""Run integration tests with a speckle server."""
import os
from pathlib import Path
from typing import Dict

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