Compare commits

..

49 Commits

Author SHA1 Message Date
Iain Sproat e26e1077e0 Merge remote-tracking branch 'template/main' into github-template-update 2022-08-12 17:54:32 +01:00
Iain Sproat aa739c30c6 fix(pull request template): pR template should be the default and not an option
PR template was in a directory which allows selection using queries.  The PR template should be
provided by default so should be renamed and placed in the .github directory.
2022-08-12 17:34:08 +01:00
Iain Sproat 0a321c66fe Remove redundant issue template 2022-08-09 09:37:28 +01:00
Iain Sproat c112f46f01 Merge remote-tracking branch 'template/main' into github-template-update 2022-08-09 09:34:41 +01:00
Iain Sproat a4f764e178 Merge remote-tracking branch 'template/main' into github-template-update 2022-08-09 09:33:55 +01:00
Iain Sproat 59f5ee5452 chore(pr_template): adds a reference section to the PR template
The SpecklePY PR template had a reference section, and it made sense to include it for all
repositories.
2022-08-09 09:32:49 +01:00
Iain Sproat f8b057b990 Refer to the code of conduct in the contributing section of the README 2022-08-08 15:11:09 +01:00
Iain Sproat e2ba8b144a Add a SECURITY.md file 2022-08-08 10:00:36 +01:00
Iain Sproat 8d320abe00 style: tidy newlines and other small formatting 2022-08-08 09:41:18 +01:00
Iain Sproat b77e346736 Merge pull request #3 from specklesystems/revise-issue-templates
Feature: separates issue template into bugs and feature requests
2022-07-27 16:02:55 +01:00
Iain Sproat 0d1c2735d8 checklist is clearer 2022-07-21 17:14:16 +01:00
Iain Sproat 8f3a683851 Retain some sections from previous issue template 2022-07-21 17:08:34 +01:00
Iain Sproat aa8c7b6f42 Add link to contribution guidelines 2022-07-21 17:03:08 +01:00
Iain Sproat 68036ee130 Feature: separates issue template into bugs and feature requests
* Provides checklist for both issue templates
* Hides instructions in comments
2022-07-21 16:56:10 +01:00
Iain Sproat 447f28c9f1 Merge pull request #2 from specklesystems/revise-pr-template
Fix: PR template updated to provide detailed instructions
2022-07-21 13:08:23 +01:00
Iain Sproat 1e7291277e Fix link to relative to the repo pull requests 2022-07-21 13:06:25 +01:00
Iain Sproat 46773aa9d3 Add link to speckle-server contribution guide 2022-07-21 12:54:41 +01:00
Iain Sproat 480ea91ebb Fixes: PR template updated to provide detailed instructions 2022-07-21 12:43:28 +01:00
Matteo Cominetti 1c0d6ce8f4 Create close-issue.yml 2021-10-02 17:03:18 +01:00
Matteo Cominetti 1431e306b8 Create open-issue.yml 2021-10-02 17:02:55 +01:00
Dimitrie Stefanescu 83bca13c8b Update README.md 2021-05-23 16:28:34 +01:00
Matteo Cominetti 1bcef9faf6 docs: adds link to docs 2021-02-19 18:40:56 +00:00
Matteo Cominetti 8d3e511d18 docs: removes links to slack 2021-01-06 16:45:48 +00:00
Alan Rynne 162f999100 fix: added yaml frontmatter block to issue template 2020-10-05 17:48:16 +02:00
Alan Rynne 2765c4fa69 Merge pull request #1 from specklesystems/alan/github-folder
Moved relevant files to .github/ folder
2020-10-05 17:35:52 +02:00
Alan Rynne 69cb2c79c7 fix: updated old link 2020-10-05 17:01:14 +02:00
Alan Rynne e2daad36e9 feat: added PR template
Updated docs to reflect it.
2020-10-05 16:58:20 +02:00
Alan Rynne d6b06298ed refactor: moved files to .github/ folder 2020-10-05 16:56:52 +02:00
Matteo Cominetti 7ddd827340 fix: more links 2020-08-21 17:56:51 +01:00
Matteo Cominetti 2a30278e04 fix: link and typos 2020-08-21 17:52:00 +01:00
Dimitrie Stefanescu ecd9089e29 Update README.md 2020-08-21 19:08:32 +03:00
izzy lyseggen bcecaef380 docs: add slack link and badge 2020-08-20 17:11:53 +01:00
Dimitrie Stefanescu 8e986e59aa Update CODE_OF_CONDUCT.md 2020-08-20 18:45:50 +03:00
Dimitrie Stefanescu 9e110a125b Update CONTRIBUTING.md
fixes link
2020-08-20 18:45:35 +03:00
Dimitrie Stefanescu ec8635401b Update README.md 2020-08-20 18:44:18 +03:00
Dimitrie Stefanescu f7b867c219 Update README.md 2020-08-20 18:37:15 +03:00
Dimitrie Stefanescu e69310619e Create LICENSE 2020-08-20 18:21:43 +03:00
Dimitrie Stefanescu 4a924593b3 Update README.md 2020-08-20 18:16:52 +03:00
Dimitrie Stefanescu 1f57e81ddc Update README.md 2020-08-20 18:16:14 +03:00
Dimitrie Stefanescu e42a3d4147 Update README.md 2020-08-20 18:04:36 +03:00
Dimitrie Stefanescu d3d53ef6a5 Update README.md 2020-08-20 18:01:04 +03:00
Dimitrie Stefanescu acb7156bf2 Update README.md
adds basic default social badges - discourse and twitter
2020-08-20 17:56:41 +03:00
Dimitrie Stefanescu 42cda6a477 Update and rename CONTRIBUTING.MD to CONTRIBUTING.md 2020-08-20 17:44:23 +03:00
Dimitrie Stefanescu c1dfe5f11f Update CODE_OF_CONDUCT.md 2020-08-20 17:41:43 +03:00
Dimitrie Stefanescu 7e57b4cfb6 Create ISSUE_TEMPLATE.md 2020-08-20 17:40:39 +03:00
Dimitrie Stefanescu b87237b88f Update CODE_OF_CONDUCT.md
adds authoritative source notice to this repo
2020-08-20 17:28:11 +03:00
Dimitrie Stefanescu fb797e64cb Create CONTRIBUTING.MD 2020-08-20 17:25:04 +03:00
Dimitrie Stefanescu 040a49baea Create CODE_OF_CONDUCT.md 2020-08-20 17:15:20 +03:00
Dimitrie Stefanescu 105ae0316c Initial commit 2020-08-20 17:11:10 +03:00
171 changed files with 4271 additions and 12710 deletions
+36 -65
View File
@@ -1,76 +1,53 @@
version: 2.1 version: 2.1
orbs: orbs:
codecov: codecov/codecov@3.3.0 python: circleci/python@2.0.3
codecov: codecov/codecov@3.2.2
jobs: jobs:
pre-commit:
parameters:
config_file:
default: ./.pre-commit-config.yaml
description: Optional, path to pre-commit config file.
type: string
cache_prefix:
default: ''
description: |
Optional cache prefix to be used on CircleCI. Can be used for cache busting or to ensure multiple jobs use different caches.
type: string
docker:
- image: speckle/pre-commit-runner:latest
resource_class: medium
steps:
- checkout
- restore_cache:
keys:
- cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
- run:
name: Install pre-commit hooks
command: pre-commit install-hooks --config <<parameters.config_file>>
- save_cache:
key: cache-pre-commit-<<parameters.cache_prefix>>-{{ checksum "<<parameters.config_file>>" }}
paths:
- ~/.cache/pre-commit
- run:
name: Run pre-commit
command: pre-commit run --all-files
- run:
command: git --no-pager diff
name: git diff
when: on_fail
test: test:
machine: docker:
image: ubuntu-2204:2023.02.1 - image: "cimg/python:<<parameters.tag>>"
docker_layer_caching: false - image: "cimg/node:16.15"
resource_class: medium - image: "cimg/redis:6.2"
- image: "cimg/postgres:14.2"
environment:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
- image: "speckle/speckle-server"
command: ["bash", "-c", "/wait && node bin/www"]
environment:
POSTGRES_URL: "localhost"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle2_test"
REDIS_URL: "redis://localhost"
SESSION_SECRET: "keyboard cat"
STRATEGY_LOCAL: "true"
CANONICAL_URL: "http://localhost:3000"
WAIT_HOSTS: localhost:5432, localhost:6379
DISABLE_FILE_UPLOADS: "true"
parameters: parameters:
tag: tag:
default: "3.11" default: "3.8"
type: string type: string
steps: steps:
- checkout - checkout
- run: python --version
- run: - run:
name: Install python command: python -m pip install --upgrade pip
command: | name: upgrade pip
pyenv install -s << parameters.tag >> - python/install-packages:
pyenv global << parameters.tag >> pkg-manager: poetry
- run: - run: poetry run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
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: - store_test_results:
path: reports path: reports
- store_artifacts: - store_artifacts:
path: reports path: reports
- codecov/upload - codecov/upload
deploy: deploy:
@@ -80,26 +57,20 @@ jobs:
- checkout - checkout
- run: python patch_version.py $CIRCLE_TAG - run: python patch_version.py $CIRCLE_TAG
- run: poetry build - run: poetry build
- run: poetry publish -u __token__ -p $PYPI_TOKEN - run: poetry publish -u specklesystems -p $PYPI_PASSWORD
workflows: workflows:
main: main:
jobs: jobs:
- pre-commit:
filters:
tags:
only: /.*/
- test: - test:
matrix: matrix:
parameters: parameters:
tag: ["3.11"] tag: ["3.7", "3.8", "3.9", "3.10"]
filters: filters:
tags: tags:
only: /.*/ only: /.*/
- deploy: - deploy:
context: pypi
requires: requires:
- pre-commit
- test - test
filters: filters:
tags: tags:
+1 -1
View File
@@ -22,6 +22,6 @@ RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/
USER vscode USER vscode
RUN curl -sSL https://install.python-poetry.org | python3 - RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python -
ENV PATH=$PATH:$HOME/.poetry/env ENV PATH=$PATH:$HOME/.poetry/env
+77
View File
@@ -0,0 +1,77 @@
name: Update issue Status
on:
issues:
types: [closed]
jobs:
update_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo "$PROJECT_ID"
echo "$STATUS_FIELD_ID"
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
echo "$DONE_ID"
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Update Status
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
set_status: updateProjectNextItemField(
input: {
projectId: $project
itemId: $id
fieldId: $status
value: $value
}
) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
+50
View File
@@ -0,0 +1,50 @@
name: Move new issues into Project
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
- name: Add Issue to project
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
-33
View File
@@ -1,33 +0,0 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
hooks:
- id: ruff
rev: v0.1.6
- repo: https://github.com/commitizen-tools/commitizen
hooks:
- id: commitizen
- id: commitizen-branch
stages:
- 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
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
+2 -3
View File
@@ -5,7 +5,6 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python: Current File", "name": "Python: Current File",
"type": "python", "type": "python",
@@ -18,8 +17,8 @@
"name": "Pytest", "name": "Pytest",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"program": "pytest", "program": "poetry",
"args": [], "args": ["run", "pytest"],
"console": "integratedTerminal", "console": "integratedTerminal",
"justMyCode": true "justMyCode": true
} }
+36 -12
View File
@@ -2,16 +2,46 @@
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/> <img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
Speckle | specklepy 🐍 Speckle | specklepy 🐍
</h1> </h1>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
<h3 align="center"> <h3 align="center">
The Python SDK The Python SDK
</h3> </h3>
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://github.com/specklesystems/specklepy/"><img src="https://circleci.com/gh/specklesystems/specklepy.svg?style=svg&amp;circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a><a href="https://codecov.io/gh/specklesystems/specklepy">
<img src="https://codecov.io/gh/specklesystems/specklepy/branch/main/graph/badge.svg?token=8KQFL5N0YF"/>
</a> </p>
# About Speckle
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
### Features
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
- **Collaboration:** share your designs collaborate with others
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
- **Real time:** get real time updates and notifications and changes
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/dev/) reference on almost any end-user and developer functionality
<p align="center"><a href="https://codecov.io/gh/specklesystems/specklepy"><img src="https://codecov.io/gh/specklesystems/specklepy/branch/main/graph/badge.svg?token=8KQFL5N0YF" alt="Codecov"></a></p>
# Repo structure # Repo structure
@@ -35,12 +65,6 @@ 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. Poetry will play along an recognize if it is invoked from inside a virtual environment.
### Style guide
All our repo wide styling linting and other rules are checked and enforced by `pre-commit`, which is included in the dev dependencies.
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 ### 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: 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:
-120
View File
@@ -1,120 +0,0 @@
version: "3.9"
name: "speckle-server"
services:
####
# Speckle Server dependencies
#######
postgres:
image: "postgres:14.5-alpine"
restart: always
environment:
POSTGRES_DB: speckle
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- postgres-data:/var/lib/postgresql/data/
healthcheck:
# the -U user has to match the POSTGRES_USER value
test: ["CMD-SHELL", "pg_isready -U speckle"]
interval: 5s
timeout: 5s
retries: 30
redis:
image: "redis:6.0-alpine"
restart: always
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 5s
timeout: 5s
retries: 30
minio:
image: "minio/minio:RELEASE.2023-10-25T06-33-25Z"
command: server /data --console-address ":9001"
restart: always
volumes:
- minio-data:/data
healthcheck:
test:
[
"CMD-SHELL",
"curl -s -o /dev/null http://127.0.0.1:9000/minio/index.html",
]
interval: 5s
timeout: 30s
retries: 30
start_period: 10s
####
# Speckle Server
#######
speckle-frontend:
image: speckle/speckle-frontend-2:latest
restart: always
ports:
- "0.0.0.0:8080:8080"
speckle-server:
image: speckle/speckle-server:latest
restart: always
healthcheck:
test:
- CMD
- /nodejs/bin/node
- -e
- "try { require('node:http').request({headers: {'Content-Type': 'application/json'}, port:3000, hostname:'127.0.0.1', path:'/readiness', method: 'GET', timeout: 2000 }, (res) => { body = ''; res.on('data', (chunk) => {body += chunk;}); res.on('end', () => {process.exit(res.statusCode != 200 || body.toLowerCase().includes('error'));}); }).end(); } catch { process.exit(1); }"
interval: 10s
timeout: 10s
retries: 3
start_period: 90s
ports:
- "0.0.0.0:3000:3000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
environment:
# TODO: Change this to the URL of the speckle server, as accessed from the network
CANONICAL_URL: "http://127.0.0.1:8080"
SPECKLE_AUTOMATE_URL: "http://127.0.0.1:3030"
# TODO: Change thvolumes:
REDIS_URL: "redis://redis"
S3_ENDPOINT: "http://minio:9000"
S3_ACCESS_KEY: "minioadmin"
S3_SECRET_KEY: "minioadmin"
S3_BUCKET: "speckle-server"
S3_CREATE_BUCKET: "true"
FILE_SIZE_LIMIT_MB: 100
MAX_PROJECT_MODELS_PER_PAGE: 500
# TODO: Change this to a unique secret for this server
SESSION_SECRET: "TODO:ReplaceWithLongString"
STRATEGY_LOCAL: "true"
DEBUG: "speckle:*"
POSTGRES_URL: "postgres"
POSTGRES_USER: "speckle"
POSTGRES_PASSWORD: "speckle"
POSTGRES_DB: "speckle"
ENABLE_MP: "false"
networks:
default:
name: speckle-server
volumes:
postgres-data:
redis-data:
minio-data:
+8 -8
View File
@@ -1,13 +1,12 @@
import os from typing import List
import random from specklepy.objects import Base
import string from specklepy.api import operations
from specklepy.transports.sqlite import SQLiteTransport
import time import time
from pathlib import Path from pathlib import Path
from typing import List import os
import string
from specklepy.api import operations import random
from specklepy.objects import Base
from specklepy.transports.sqlite import SQLiteTransport
class Sub(Base): class Sub(Base):
@@ -27,6 +26,7 @@ def clean_db():
def one_pass(clean: bool, randomize: bool, child_count: int): def one_pass(clean: bool, randomize: bool, child_count: int):
foo = Base() foo = Base()
for i in range(child_count): for i in range(child_count):
stuff = random_string() if randomize else "stuff" stuff = random_string() if randomize else "stuff"
-15
View File
@@ -1,15 +0,0 @@
from devtools import debug
from specklepy.api import operations
from specklepy.api.wrapper import StreamWrapper
if __name__ == "__main__":
stream_url = "https://latest.speckle.dev/streams/7d051a6449"
wrapper = StreamWrapper(stream_url)
transport = wrapper.get_transport()
rec = operations.receive("98396753f8bf7fe1cb60c5193e9f9d86", transport)
# hash = operations.send(base=foo, transports=[transport], use_default_cache=False)
debug(rec)
-39
View File
@@ -1,39 +0,0 @@
import random
import string
from typing import List
from specklepy.api import operations
from specklepy.api.wrapper import StreamWrapper
from specklepy.objects import Base
class Sub(Base):
bar: List[str]
def random_string():
letters = string.ascii_lowercase
return "".join(random.choice(letters) for _ in range(10))
def create_object(child_count: int) -> Base:
foo = Base()
for i in range(child_count):
stuff = random_string()
foo[f"@child_{i}"] = Sub(bar=["asdf", "bar", i, stuff])
return foo
if __name__ == "__main__":
stream_url = "http://hyperion:3000/streams/2372b54c35"
child_count = 10
foo = create_object(child_count)
wrapper = StreamWrapper(stream_url)
transport = wrapper.get_transport()
hash = operations.send(base=foo, transports=[transport], use_default_cache=False)
rec = operations.receive(hash, transport)
print(rec)
-36
View File
@@ -1,36 +0,0 @@
from devtools import debug
from specklepy.api import operations
from specklepy.objects.geometry import Base
from specklepy.objects.units import Units
dct = {
"id": "1234abcd",
"units": None,
"speckle_type": "Base",
"applicationId": None,
"totalChildrenCount": 0,
}
base = Base()
for prop, value in dct.items():
base.__setattr__(prop, value)
debug(base)
debug(base.units)
base.units = "m"
debug(base.units)
base.units = None
debug(base.units)
foo = operations.serialize(base)
base.units = 10
debug(base.units)
debug(foo)
base.units = Units.mm
debug(base.units)
+2 -3
View File
@@ -1,10 +1,9 @@
"""This is an example showcasing the usage of speckle `Base` class.""" """This is an example showcasing the usage of speckle `Base` class."""
# the speckle.objects module exposes all speckle provided classes # the speckle.objects module exposes all speckle provided classes
from devtools import debug
from specklepy.api import operations
from specklepy.objects import Base from specklepy.objects import Base
from specklepy.api import operations
from devtools import debug
class ExampleSub(Base): class ExampleSub(Base):
+1 -1
View File
@@ -9,7 +9,7 @@ def patch(tag):
lines = f.readlines() lines = f.readlines()
if "version" not in lines[2]: if "version" not in lines[2]:
raise Exception("Invalid pyproject.toml. Could not patch version.") raise Exception(f"Invalid pyproject.toml. Could not patch version.")
lines[2] = f'version = "{tag}"\n' lines[2] = f'version = "{tag}"\n'
with open("pyproject.toml", "w") as file: with open("pyproject.toml", "w") as file:
Generated
+885 -1683
View File
File diff suppressed because it is too large Load Diff
+9 -31
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "specklepy" name = "specklepy"
version = "2.17.14" version = "2.4.0"
description = "The Python SDK for Speckle 2.0" description = "The Python SDK for Speckle 2.0"
readme = "README.md" readme = "README.md"
authors = ["Speckle Systems <devops@speckle.systems>"] authors = ["Speckle Systems <devops@speckle.systems>"]
@@ -8,40 +8,25 @@ license = "Apache-2.0"
repository = "https://github.com/specklesystems/speckle-py" repository = "https://github.com/specklesystems/speckle-py"
documentation = "https://speckle.guide/dev/py-examples.html" documentation = "https://speckle.guide/dev/py-examples.html"
homepage = "https://speckle.systems/" homepage = "https://speckle.systems/"
packages = [
{ include = "specklepy", from = "src" },
{ include = "speckle_automate", from = "src" },
]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.8.0, <4.0" python = ">=3.7.2, <4.0"
pydantic = "^2.5" pydantic = "^1.8.2"
appdirs = "^1.4.4" appdirs = "^1.4.4"
gql = { extras = ["requests", "websockets"], version = "^3.3.0" } gql = {extras = ["requests", "websockets"], version = "^3.3.0"}
ujson = "^5.3.0" ujson = "^5.3.0"
Deprecated = "^1.2.13" Deprecated = "^1.2.13"
stringcase = "^1.2.0"
attrs = "^23.1.0"
httpx = "^0.25.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.dev-dependencies]
black = "23.11.0" black = "^20.8b1"
isort = "^5.7.0" isort = "^5.7.0"
pytest = "^7.1.3" pytest = "^6.2.2"
pytest-asyncio = "^0.23.0"
pytest-ordering = "^0.6" pytest-ordering = "^0.6"
pytest-cov = "^3.0.0" pytest-cov = "^3.0.0"
devtools = "^0.8.0" devtools = "^0.8.0"
pylint = "^2.14.4" 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] [tool.black]
exclude = ''' exclude = '''
@@ -60,16 +45,9 @@ exclude = '''
''' '''
include = '\.pyi?$' include = '\.pyi?$'
line-length = 88 line-length = 88
target-version = ["py37", "py38", "py39", "py310", "py311"] target-version = ["py37", "py38", "py39", "py310"]
[tool.commitizen]
name = "cz_conventional_commits"
version = "2.9.2"
tag_format = "$version"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.isort]
profile = "black"
@@ -1,44 +1,36 @@
import re import re
from typing import Dict
from warnings import warn from warnings import warn
from deprecated import deprecated from deprecated import deprecated
from gql import Client from specklepy.api.credentials import Account, get_account_from_token
from gql.transport.exceptions import TransportServerError from specklepy.logging import metrics
from gql.transport.requests import RequestsHTTPTransport from specklepy.logging.exceptions import (
from gql.transport.websockets import WebsocketsTransport SpeckleException,
SpeckleWarning,
)
from typing import Dict
from specklepy.core.api import resources from specklepy.api import resources
from specklepy.core.api.credentials import Account, get_account_from_token from specklepy.api.resources import (
from specklepy.core.api.resources import (
ActiveUserResource,
ModelResource,
OtherUserResource,
ProjectInviteResource,
ProjectResource,
ServerResource,
SubscriptionResource,
VersionResource,
branch, branch,
commit, commit,
object,
stream, stream,
subscriptions, object,
server,
user, user,
subscriptions,
) )
from specklepy.logging import metrics from specklepy.api.models import ServerInfo
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning from gql import Client
from gql.transport.requests import RequestsHTTPTransport
from gql.transport.websockets import WebsocketsTransport
class SpeckleClient: class SpeckleClient:
""" """
The `SpeckleClient` is your entry point for interacting with The `SpeckleClient` is your entry point for interacting with your Speckle Server's GraphQL API.
your Speckle Server's GraphQL API. You'll need to have access to a server to use it, or you can use our public server `speckle.xyz`.
You'll need to have access to a server to use it,
or you can use our public server `app.speckle.systems`.
To authenticate the client, you'll need to have downloaded To authenticate the client, you'll need to have downloaded the [Speckle Manager](https://speckle.guide/#speckle-manager)
the [Speckle Manager](https://speckle.guide/#speckle-manager)
and added your account. and added your account.
```py ```py
@@ -46,7 +38,7 @@ class SpeckleClient:
from specklepy.api.credentials import get_default_account from specklepy.api.credentials import get_default_account
# initialise the client # initialise the client
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is client = SpeckleClient(host="speckle.xyz") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server # 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)
@@ -61,17 +53,11 @@ class SpeckleClient:
``` ```
""" """
DEFAULT_HOST = "app.speckle.systems" DEFAULT_HOST = "speckle.xyz"
USE_SSL = True USE_SSL = True
def __init__( def __init__(self, host: str = DEFAULT_HOST, use_ssl: bool = USE_SSL) -> None:
self, metrics.track(metrics.CLIENT, custom_props={"name": "create"})
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
connection_retries: int = 3,
connection_timeout: int = 10,
) -> None:
ws_protocol = "ws" ws_protocol = "ws"
http_protocol = "http" http_protocol = "http"
@@ -86,17 +72,9 @@ class SpeckleClient:
self.graphql = f"{self.url}/graphql" self.graphql = f"{self.url}/graphql"
self.ws_url = f"{ws_protocol}://{host}/graphql" self.ws_url = f"{ws_protocol}://{host}/graphql"
self.account = Account() self.account = Account()
self.verify_certificate = verify_certificate
self.connection_retries = connection_retries
self.connection_timeout = connection_timeout
self.httpclient = Client( self.httpclient = Client(
transport=RequestsHTTPTransport( transport=RequestsHTTPTransport(url=self.graphql, verify=True, retries=3)
url=self.graphql,
verify=self.verify_certificate,
retries=self.connection_retries,
timeout=self.connection_timeout,
)
) )
self.wsclient = None self.wsclient = None
@@ -115,61 +93,52 @@ class SpeckleClient:
# ) from ex # ) from ex
def __repr__(self): def __repr__(self):
return ( return f"SpeckleClient( server: {self.url}, authenticated: {self.account.token is not None} )"
f"SpeckleClient( server: {self.url}, authenticated:"
f" {self.account.token is not None} )"
)
@deprecated( @deprecated(
version="2.6.0", version="2.6.0",
reason=( reason="Renamed: please use `authenticate_with_account` or `authenticate_with_token` instead.",
"Renamed: please use `authenticate_with_account` or"
" `authenticate_with_token` instead."
),
) )
def authenticate(self, token: str) -> None: def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token """Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL The token is saved in the client object and a synchronous GraphQL entrypoint is created
entrypoint is created
Arguments: Arguments:
token {str} -- an api token token {str} -- an api token
""" """
self.authenticate_with_account(get_account_from_token(token)) self.authenticate_with_token(token)
self._set_up_client()
def authenticate_with_token(self, token: str) -> None: def authenticate_with_token(self, token: str) -> None:
""" """Authenticate the client using a personal access token
Authenticate the client using a personal access token. The token is saved in the client object and a synchronous GraphQL entrypoint is created
The token is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments: Arguments:
token {str} -- an api token token {str} -- an api token
""" """
self.account = Account.from_token(token, self.url) self.account = get_account_from_token(token, self.url)
metrics.track(metrics.CLIENT, self.account, {"name": "authenticate with token"})
self._set_up_client() self._set_up_client()
def authenticate_with_account(self, account: Account) -> None: def authenticate_with_account(self, account: Account) -> None:
"""Authenticate the client using an Account object """Authenticate the client using an Account object
The account is saved in the client object and a synchronous GraphQL The account is saved in the client object and a synchronous GraphQL entrypoint is created
entrypoint is created
Arguments: Arguments:
account {Account} -- the account object which can be found with account {Account} -- the account object which can be found with `get_default_account` or `get_local_accounts`
`get_default_account` or `get_local_accounts`
""" """
metrics.track(metrics.CLIENT, account, {"name": "authenticate with account"})
self.account = account self.account = account
self._set_up_client() self._set_up_client()
def _set_up_client(self) -> None: def _set_up_client(self) -> None:
metrics.track(metrics.CLIENT, self.account, {"name": "set up client"})
headers = { headers = {
"Authorization": f"Bearer {self.account.token}", "Authorization": f"Bearer {self.account.token}",
"Content-Type": "application/json", "Content-Type": "application/json",
"apollographql-client-name": metrics.HOST_APP,
"apollographql-client-version": metrics.HOST_APP_VERSION,
} }
httptransport = RequestsHTTPTransport( httptransport = RequestsHTTPTransport(
url=self.graphql, headers=headers, verify=self.verify_certificate, retries=3 url=self.graphql, headers=headers, verify=True, retries=3
) )
wstransport = WebsocketsTransport( wstransport = WebsocketsTransport(
url=self.ws_url, url=self.ws_url,
@@ -180,76 +149,25 @@ class SpeckleClient:
self._init_resources() self._init_resources()
try: if self.user.get() is None:
_ = self.active_user.get() warn(
except SpeckleException as ex: SpeckleWarning(
if isinstance(ex.exception, TransportServerError): f"Possibly invalid token - could not authenticate Speckle Client for server {self.url}"
if ex.exception.code == 403: )
warn( )
SpeckleWarning(
"Possibly invalid token - could not authenticate Speckle Client"
f" for server {self.url}"
)
)
else:
raise ex
def execute_query(self, query: str) -> Dict: def execute_query(self, query: str) -> Dict:
return self.httpclient.execute(query) return self.httpclient.execute(query)
def _init_resources(self) -> None: def _init_resources(self) -> None:
self.server = ServerResource( self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient account=self.account, basepath=self.url, client=self.httpclient
) )
server_version = None server_version = None
try: try:
server_version = self.server.version() server_version = self.server.version()
except Exception: except:
pass pass
self.other_user = OtherUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = ActiveUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project = ProjectResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project_invite = ProjectInviteResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.model = ModelResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.version = VersionResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.subscription = SubscriptionResource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
)
# Deprecated Resources
self.user = user.Resource( self.user = user.Resource(
account=self.account, account=self.account,
basepath=self.url, basepath=self.url,
@@ -283,7 +201,7 @@ class SpeckleClient:
return attr.Resource( return attr.Resource(
account=self.account, basepath=self.url, client=self.httpclient account=self.account, basepath=self.url, client=self.httpclient
) )
except AttributeError: except:
raise SpeckleException( raise SpeckleException(
f"Method {name} is not supported by the SpeckleClient class" f"Method {name} is not supported by the SpeckleClient class"
) )
@@ -1,22 +1,17 @@
import os import os
from pathlib import Path
from typing import List, Optional
from urllib.parse import urlparse
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
from typing import List, Optional
from specklepy.core.api.models import ServerInfo from specklepy.logging import metrics
from specklepy.core.helpers import speckle_path_provider from specklepy.api.models import ServerInfo
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.sqlite import SQLiteTransport from specklepy.transports.sqlite import SQLiteTransport
from specklepy.logging.exceptions import SpeckleException
class UserInfo(BaseModel): class UserInfo(BaseModel):
id: Optional[str] = None name: Optional[str]
name: Optional[str] = None email: Optional[str]
email: Optional[str] = None company: Optional[str]
company: Optional[str] = None id: Optional[str]
avatar: Optional[str] = None
class Account(BaseModel): class Account(BaseModel):
@@ -28,10 +23,7 @@ class Account(BaseModel):
id: Optional[str] = None id: Optional[str] = None
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url}, isDefault: {self.isDefault})"
f"Account(email: {self.userInfo.email}, server: {self.serverInfo.url},"
f" isDefault: {self.isDefault})"
)
def __str__(self) -> str: def __str__(self) -> str:
return self.__repr__() return self.__repr__()
@@ -43,45 +35,31 @@ class Account(BaseModel):
return acct return acct
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]: def get_local_accounts(base_path: str = None) -> List[Account]:
"""Gets all the accounts present in this environment """Gets all the accounts present in this environment
Arguments: Arguments:
base_path {str} -- custom base path if you are not using the system default base_path {str} -- custom base path if you are not using the system default
Returns: Returns:
List[Account] -- list of all local accounts or an empty list if List[Account] -- list of all local accounts or an empty list if no accounts were found
no accounts were found
""" """
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path)
# pylint: disable=protected-access
json_path = os.path.join(account_storage._base_path, "Accounts")
os.makedirs(json_path, exist_ok=True)
json_acct_files = [file for file in os.listdir(json_path) if file.endswith(".json")]
accounts: List[Account] = [] accounts: List[Account] = []
try: res = account_storage.get_all_objects()
account_storage = SQLiteTransport(scope="Accounts", base_path=base_path) account_storage.close()
res = account_storage.get_all_objects()
account_storage.close()
if res:
accounts.extend(Account.model_validate_json(r[1]) for r in res)
except SpeckleException:
# cannot open SQLiteTransport, probably because of the lack
# of disk write permissions
pass
json_acct_files = []
json_path = str(speckle_path_provider.accounts_folder_path())
try:
os.makedirs(json_path, exist_ok=True)
json_acct_files.extend(
file for file in os.listdir(json_path) if file.endswith(".json")
)
except Exception:
# cannot find or get the json account paths
pass
if res:
accounts.extend(Account.parse_raw(r[1]) for r in res)
if json_acct_files: if json_acct_files:
try: try:
accounts.extend( accounts.extend(
Account.model_validate_json(Path(json_path, json_file).read_text()) Account.parse_file(os.path.join(json_path, json_file))
# Account.parse_file(os.path.join(json_path, json_file))
for json_file in json_acct_files for json_file in json_acct_files
) )
except Exception as ex: except Exception as ex:
@@ -90,13 +68,19 @@ def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
ex, ex,
) from ex ) from ex
metrics.track(
metrics.ACCOUNTS,
next(
(acc for acc in accounts if acc.isDefault),
accounts[0] if accounts else None,
),
)
return accounts return accounts
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]: def get_default_account(base_path: str = None) -> Account:
""" """Gets this environment's default account if any. If there is no default, the first found will be returned and set as default.
Gets this environment's default account if any. If there is no default,
the first found will be returned and set as default.
Arguments: Arguments:
base_path {str} -- custom base path if you are not using the system default base_path {str} -- custom base path if you are not using the system default
@@ -111,7 +95,7 @@ def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
if not default: if not default:
default = accounts[0] default = accounts[0]
default.isDefault = True default.isDefault = True
# metrics.initialise_tracker(default) metrics.initialise_tracker(default)
return default return default
@@ -122,8 +106,7 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
token {str} -- the api token token {str} -- the api token
Returns: Returns:
Account -- the local account with this token or a shell account containing Account -- the local account with this token or a shell account containing just the token and url if no local account is found
just the token and url if no local account is found
""" """
accounts = get_local_accounts() accounts = get_local_accounts()
if not accounts: if not accounts:
@@ -144,34 +127,9 @@ def get_account_from_token(token: str, server_url: str = None) -> Account:
return Account.from_token(token, server_url) return Account.from_token(token, server_url)
def get_accounts_for_server(host: str) -> List[Account]:
all_accounts = get_local_accounts()
filtered: List[Account] = []
for acc in all_accounts:
moved_from = (
acc.serverInfo.migration.movedFrom if acc.serverInfo.migration else None
)
if moved_from and host == urlparse(moved_from).netloc:
filtered.append(acc)
for acc in all_accounts:
if any([x for x in filtered if x.userInfo.id == acc.userInfo.id]):
continue
if host == urlparse(acc.serverInfo.url).netloc:
filtered.append(acc)
return filtered
class StreamWrapper: class StreamWrapper:
def __init__(self, url: str = None) -> None: def __init__(self, url: str = None) -> None:
raise SpeckleException( raise SpeckleException(
message=( message="The StreamWrapper has moved as of v2.6.0! Please import from specklepy.api.wrapper",
"The StreamWrapper has moved as of v2.6.0! Please import from" exception=DeprecationWarning,
" specklepy.api.wrapper"
),
exception=DeprecationWarning(),
) )
+170
View File
@@ -0,0 +1,170 @@
# generated by datamodel-codegen:
# filename: stream_schema.json
# timestamp: 2020-11-17T14:33:13+00:00
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel # pylint: disable=no-name-in-module
class Collaborator(BaseModel):
id: Optional[str]
name: Optional[str]
role: Optional[str]
avatar: Optional[str]
class Commit(BaseModel):
id: Optional[str]
message: Optional[str]
authorName: Optional[str]
authorId: Optional[str]
authorAvatar: Optional[str]
branchName: Optional[str]
createdAt: Optional[datetime]
sourceApplication: Optional[str]
referencedObject: Optional[str]
totalChildrenCount: Optional[int]
parents: Optional[List[str]]
def __repr__(self) -> str:
return f"Commit( id: {self.id}, message: {self.message}, referencedObject: {self.referencedObject}, authorName: {self.authorName}, branchName: {self.branchName}, createdAt: {self.createdAt} )"
def __str__(self) -> str:
return self.__repr__()
class Commits(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Commit] = []
class Object(BaseModel):
id: Optional[str]
speckleType: Optional[str]
applicationId: Optional[str]
totalChildrenCount: Optional[int]
createdAt: Optional[datetime]
class Branch(BaseModel):
id: Optional[str]
name: Optional[str]
description: Optional[str]
commits: Optional[Commits]
class Branches(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Branch] = []
class Stream(BaseModel):
id: Optional[str]
name: Optional[str]
role: Optional[str]
isPublic: Optional[bool]
description: Optional[str]
createdAt: Optional[datetime]
updatedAt: Optional[datetime]
collaborators: List[Collaborator] = []
branches: Optional[Branches]
commit: Optional[Commit]
object: Optional[Object]
commentCount: Optional[int]
favoritedDate: Optional[datetime]
favoritesCount: Optional[int]
def __repr__(self):
return f"Stream( id: {self.id}, name: {self.name}, description: {self.description}, isPublic: {self.isPublic})"
def __str__(self) -> str:
return self.__repr__()
class Streams(BaseModel):
totalCount: Optional[int]
cursor: Optional[datetime]
items: List[Stream] = []
class User(BaseModel):
id: Optional[str]
email: Optional[str]
name: Optional[str]
bio: Optional[str]
company: Optional[str]
avatar: Optional[str]
verified: Optional[bool]
role: Optional[str]
streams: Optional[Streams]
def __repr__(self):
return f"User( id: {self.id}, name: {self.name}, email: {self.email}, company: {self.company} )"
def __str__(self) -> str:
return self.__repr__()
class PendingStreamCollaborator(BaseModel):
id: Optional[str]
inviteId: Optional[str]
streamId: Optional[str]
streamName: Optional[str]
title: Optional[str]
role: Optional[str]
invitedBy: Optional[User]
user: Optional[User]
token: Optional[str]
def __repr__(self):
return f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId: {self.streamId}, role: {self.role}, title: {self.title}, invitedBy: {self.user.name if self.user else None})"
def __str__(self) -> str:
return self.__repr__()
class Activity(BaseModel):
actionType: Optional[str]
info: Optional[dict]
userId: Optional[str]
streamId: Optional[str]
resourceId: Optional[str]
resourceType: Optional[str]
message: Optional[str]
time: Optional[datetime]
def __repr__(self) -> str:
return f"Activity( streamId: {self.streamId}, actionType: {self.actionType}, message: {self.message}, userId: {self.userId} )"
def __str__(self) -> str:
return self.__repr__()
class ActivityCollection(BaseModel):
totalCount: Optional[int]
items: Optional[List[Activity]]
cursor: Optional[datetime]
def __repr__(self) -> str:
return f"ActivityCollection( totalCount: {self.totalCount}, items: {len(self.items) if self.items else 0}, cursor: {self.cursor.isoformat() if self.cursor else None} )"
def __str__(self) -> str:
return self.__repr__()
class ServerInfo(BaseModel):
name: Optional[str]
company: Optional[str]
url: Optional[str]
description: Optional[str]
adminContact: Optional[str]
canonicalUrl: Optional[str]
roles: Optional[List[dict]]
scopes: Optional[List[dict]]
authStrategies: Optional[List[dict]]
version: Optional[str]
@@ -1,16 +1,16 @@
from typing import List, Optional from typing import List
from specklepy.logging import metrics
# from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base from specklepy.objects.base import Base
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.transports.sqlite import SQLiteTransport from specklepy.transports.sqlite import SQLiteTransport
from specklepy.transports.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.serialization.base_object_serializer import BaseObjectSerializer
def send( def send(
base: Base, base: Base,
transports: Optional[List[AbstractTransport]] = None, transports: List[AbstractTransport] = None,
use_default_cache: bool = True, use_default_cache: bool = True,
): ):
"""Sends an object via the provided transports. Defaults to the local cache. """Sends an object via the provided transports. Defaults to the local cache.
@@ -18,8 +18,7 @@ def send(
Arguments: Arguments:
obj {Base} -- the object you want to send obj {Base} -- the object you want to send
transports {list} -- where you want to send them transports {list} -- where you want to send them
use_default_cache {bool} -- toggle for the default cache. use_default_cache {bool} -- toggle for the default cache. If set to false, it will only send to the provided transports
If set to false, it will only send to the provided transports
Returns: Returns:
str -- the object id of the sent object str -- the object id of the sent object
@@ -27,17 +26,17 @@ def send(
if not transports and not use_default_cache: if not transports and not use_default_cache:
raise SpeckleException( raise SpeckleException(
message=( message="You need to provide at least one transport: cannot send with an empty transport list and no default cache"
"You need to provide at least one transport: cannot send with an empty"
" transport list and no default cache"
)
) )
if isinstance(transports, AbstractTransport): if isinstance(transports, AbstractTransport):
transports = [transports] transports = [transports]
if transports is None: if transports is None:
metrics.track(metrics.SEND)
transports = [] transports = []
else:
metrics.track(metrics.SEND, getattr(transports[0], "account", None))
if use_default_cache: if use_default_cache:
transports.insert(0, SQLiteTransport()) transports.insert(0, SQLiteTransport())
@@ -51,8 +50,8 @@ def send(
def receive( def receive(
obj_id: str, obj_id: str,
remote_transport: Optional[AbstractTransport] = None, remote_transport: AbstractTransport = None,
local_transport: Optional[AbstractTransport] = None, local_transport: AbstractTransport = None,
) -> Base: ) -> Base:
"""Receives an object from a transport. """Receives an object from a transport.
@@ -65,22 +64,20 @@ def receive(
Returns: Returns:
Base -- the base object Base -- the base object
""" """
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
if not local_transport: if not local_transport:
local_transport = SQLiteTransport() local_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=local_transport) 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 deserialisation using the local transport
obj_string = local_transport.get_object(obj_id) obj_string = local_transport.get_object(obj_id)
if obj_string: if obj_string:
return serializer.read_json(obj_string=obj_string) return serializer.read_json(obj_string=obj_string)
if not remote_transport: if not remote_transport:
raise SpeckleException( raise SpeckleException(
message=( message="Could not find the specified object using the local transport, and you didn't provide a fallback remote from which to pull it."
"Could not find the specified object using the local transport, and you"
" didn't provide a fallback remote from which to pull it."
)
) )
obj_string = remote_transport.copy_object_and_children( obj_string = remote_transport.copy_object_and_children(
@@ -92,48 +89,38 @@ def receive(
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str: def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
""" """
Serialize a base object. If no write transports are provided, Serialize a base object. If no write transports are provided, the object will be serialized
the object will be serialized
without detaching or chunking any of the attributes. without detaching or chunking any of the attributes.
Arguments: Arguments:
base {Base} -- the object to serialize base {Base} -- the object to serialize
write_transports {List[AbstractTransport]} write_transports {List[AbstractTransport]} -- optional: the transports to write to
-- optional: the transports to write to
Returns: Returns:
str -- the serialized object str -- the serialized object
""" """
metrics.track(metrics.SERIALIZE)
serializer = BaseObjectSerializer(write_transports=write_transports) serializer = BaseObjectSerializer(write_transports=write_transports)
return serializer.write_json(base)[1] return serializer.write_json(base)[1]
def deserialize( def deserialize(obj_string: str, read_transport: AbstractTransport = None) -> Base:
obj_string: str, read_transport: Optional[AbstractTransport] = None
) -> Base:
""" """
Deserialize a string object into a Base object. Deserialize a string object into a Base object. If the object contains referenced child objects that are not stored in the local db, a read transport needs to be provided in order to recompose the base with the children objects.
If the object contains referenced child objects that are not stored in the local db,
a read transport needs to be provided in order to recompose
the base with the children objects.
Arguments: Arguments:
obj_string {str} -- the string object to deserialize obj_string {str} -- the string object to deserialize
read_transport {AbstractTransport} read_transport {AbstractTransport} -- the transport to fetch children objects from
-- the transport to fetch children objects from (defaults to SQLiteTransport)
(defaults to SQLiteTransport)
Returns: Returns:
Base -- the deserialized object Base -- the deserialized object
""" """
metrics.track(metrics.DESERIALIZE)
if not read_transport: if not read_transport:
read_transport = SQLiteTransport() read_transport = SQLiteTransport()
serializer = BaseObjectSerializer(read_transport=read_transport) serializer = BaseObjectSerializer(read_transport=read_transport)
return serializer.read_json(obj_string=obj_string) return serializer.read_json(obj_string=obj_string)
__all__ = ["receive", "send", "serialize", "deserialize"]
@@ -1,21 +1,15 @@
from threading import Lock from graphql import DocumentNode
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union from specklepy.api.credentials import Account
from specklepy.transports.sqlite import SQLiteTransport
from typing import Any, Dict, List, Optional, Tuple, Type, Union
from gql.client import Client from gql.client import Client
from gql.transport.exceptions import TransportQueryError from gql.transport.exceptions import TransportQueryError
from graphql import DocumentNode
from pydantic import BaseModel
from specklepy.core.api.credentials import Account
from specklepy.logging.exceptions import ( from specklepy.logging.exceptions import (
GraphQLException, GraphQLException,
SpeckleException, SpeckleException,
UnsupportedException, UnsupportedException,
) )
from specklepy.serialization.base_object_serializer import BaseObjectSerializer from specklepy.serialization.base_object_serializer import BaseObjectSerializer
from specklepy.transports.sqlite import SQLiteTransport
T = TypeVar("T", bound=BaseModel)
class ResourceBase(object): class ResourceBase(object):
@@ -33,7 +27,6 @@ class ResourceBase(object):
self.name = name self.name = name
self.server_version = server_version self.server_version = server_version
self.schema: Optional[Type] = None self.schema: Optional[Type] = None
self.__lock = Lock()
def _step_into_response(self, response: dict, return_type: Union[str, List, None]): def _step_into_response(self, response: dict, return_type: Union[str, List, None]):
"""Step into the dict to get the relevant data""" """Step into the dict to get the relevant data"""
@@ -46,35 +39,6 @@ class ResourceBase(object):
response = response[key] response = response[key]
return response return response
def make_request_and_parse_response(
self,
schema: Type[T],
query: DocumentNode,
variables: Optional[Dict[str, Any]] = None,
) -> T:
try:
with self.__lock:
response = self.client.execute(query, variable_values=variables)
except TransportQueryError as ex:
raise GraphQLException(
message=(
f"Failed to execute the GraphQL {self.name} request. Errors:"
f" {ex.errors}"
),
errors=ex.errors,
data=ex.data,
) from ex
except Exception as ex:
raise SpeckleException(
message=(
f"Failed to execute the GraphQL {self.name} request. Inner"
f" exception: {ex}"
),
exception=ex,
) from ex
return schema.model_validate(response)
def _parse_response(self, response: Union[dict, list, None], schema=None): def _parse_response(self, response: Union[dict, list, None], schema=None):
"""Try to create a class instance from the response""" """Try to create a class instance from the response"""
if response is None: if response is None:
@@ -82,11 +46,11 @@ class ResourceBase(object):
if isinstance(response, list): if isinstance(response, list):
return [self._parse_response(response=r, schema=schema) for r in response] return [self._parse_response(response=r, schema=schema) for r in response]
if schema: if schema:
return schema.model_validate(response) return schema.parse_obj(response)
elif self.schema: elif self.schema:
try: try:
return self.schema.model_validate(response) return self.schema.parse_obj(response)
except Exception: except:
s = BaseObjectSerializer(read_transport=SQLiteTransport()) s = BaseObjectSerializer(read_transport=SQLiteTransport())
return s.recompose_base(response) return s.recompose_base(response)
else: else:
@@ -95,33 +59,24 @@ class ResourceBase(object):
def make_request( def make_request(
self, self,
query: DocumentNode, query: DocumentNode,
params: Optional[Dict] = None, params: Dict = None,
return_type: Union[str, List, None] = None, return_type: Union[str, List, None] = None,
schema=None, schema=None,
parse_response: bool = True, parse_response: bool = True,
) -> Any: ) -> Any:
"""Executes the GraphQL query""" """Executes the GraphQL query"""
# This method has quite complex and ambiguous typing, and counter-intuitive error handling
# We are going to phase it out in favour of `make_request_and_parse_response`
try: try:
with self.__lock: response = self.client.execute(query, variable_values=params)
response = self.client.execute(query, variable_values=params)
except Exception as ex: except Exception as ex:
if isinstance(ex, TransportQueryError): if isinstance(ex, TransportQueryError):
return GraphQLException( return GraphQLException(
message=( message=f"Failed to execute the GraphQL {self.name} request. Errors: {ex.errors}",
f"Failed to execute the GraphQL {self.name} request. Errors:"
f" {ex.errors}"
),
errors=ex.errors, errors=ex.errors,
data=ex.data, data=ex.data,
) )
else: else:
return SpeckleException( return SpeckleException(
message=( message=f"Failed to execute the GraphQL {self.name} request. Inner exception: {ex}",
f"Failed to execute the GraphQL {self.name} request. Inner"
f" exception: {ex}"
),
exception=ex, exception=ex,
) )
@@ -133,23 +88,16 @@ class ResourceBase(object):
return response return response
def _check_server_version_at_least( def _check_server_version_at_least(
self, target_version: Tuple[Any, ...], unsupported_message: Optional[str] = None self, target_version: Tuple[Any, ...], unsupported_message: str = None
): ):
"""Use this check to guard against making unsupported requests on older servers. """Use this check to guard against making unsupported requests on older servers.
Arguments: Arguments:
target_version {tuple} target_version {tuple} -- the minimum server version in the format (major, minor, patch, (tag, build))
the minimum 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
eg (2, 6, 3) for a stable build and (2, 6, 4, 'alpha', 4711) for alpha
""" """
if not unsupported_message: if not unsupported_message:
unsupported_message = ( unsupported_message = f"The client method used is not supported on Speckle Server versios prior to v{'.'.join(target_version)}"
"The client method used is not supported on Speckle Server versions"
f" prior to v{'.'.join(target_version)}"
)
# if version is dev, it should be supported... (or not)
if self.server_version == ("dev",):
return
if self.server_version and self.server_version < target_version: if self.server_version and self.server_version < target_version:
raise UnsupportedException(unsupported_message) raise UnsupportedException(unsupported_message)
@@ -159,7 +107,8 @@ class ResourceBase(object):
""" """
self._check_server_version_at_least( self._check_server_version_at_least(
(2, 6, 4), (2, 6, 4),
"Stream invites are only supported as of Speckle Server v2.6.4. Please" (
" update your Speckle Server to use this method or use the" "Stream invites are only supported as of Speckle Server v2.6.4. "
" `grant_permission` flow instead.", "Please update your Speckle Server to use this method or use the `grant_permission` flow instead."
),
) )
+12
View File
@@ -0,0 +1,12 @@
from pathlib import Path
import sys
import pkgutil
from importlib import import_module
for (_, name, _) in pkgutil.iter_modules(__path__):
imported_module = import_module("." + name, package=__name__)
if hasattr(imported_module, "Resource"):
setattr(sys.modules[__name__], name, imported_module)
@@ -1,24 +1,13 @@
from typing import Optional
from deprecated import deprecated
from gql import gql from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.core.api.models.deprecated import ( from specklepy.api.models import Branch
FE1_DEPRECATION_REASON, from specklepy.logging import metrics
FE1_DEPRECATION_VERSION,
Branch,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "branch" NAME = "branch"
class Resource(ResourceBase): class Resource(ResourceBase):
""" """API Access class for branches"""
API Access class for branches
Branch resource is deprecated, please use model resource instead
"""
def __init__(self, account, basepath, client) -> None: def __init__(self, account, basepath, client) -> None:
super().__init__( super().__init__(
@@ -29,7 +18,6 @@ class Resource(ResourceBase):
) )
self.schema = Branch self.schema = Branch
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create( def create(
self, stream_id: str, name: str, description: str = "No description provided" self, stream_id: str, name: str, description: str = "No description provided"
) -> str: ) -> str:
@@ -42,6 +30,7 @@ class Resource(ResourceBase):
Returns: Returns:
id {str} -- the newly created branch's id id {str} -- the newly created branch's id
""" """
metrics.track(metrics.BRANCH, self.account, {"name": "create"})
query = gql( query = gql(
""" """
mutation BranchCreate($branch: BranchCreateInput!) { mutation BranchCreate($branch: BranchCreateInput!) {
@@ -49,8 +38,6 @@ class Resource(ResourceBase):
} }
""" """
) )
if len(name) < 3:
return SpeckleException(message="Branch Name must be at least 3 characters")
params = { params = {
"branch": { "branch": {
"streamId": stream_id, "streamId": stream_id,
@@ -63,7 +50,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="branchCreate", parse_response=False query=query, params=params, return_type="branchCreate", parse_response=False
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, name: str, commits_limit: int = 10): def get(self, stream_id: str, name: str, commits_limit: int = 10):
"""Get a branch by name from a stream """Get a branch by name from a stream
@@ -75,6 +61,7 @@ class Resource(ResourceBase):
Returns: Returns:
Branch -- the fetched branch with its latest commits Branch -- the fetched branch with its latest commits
""" """
metrics.track(metrics.BRANCH, self.account, {"name": "get"})
query = gql( query = gql(
""" """
query BranchGet($stream_id: String!, $name: String!, $commits_limit: Int!) { query BranchGet($stream_id: String!, $name: String!, $commits_limit: Int!) {
@@ -111,7 +98,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "branch"] query=query, params=params, return_type=["stream", "branch"]
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10): def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream """Get a list of branches from a given stream
@@ -123,13 +109,10 @@ class Resource(ResourceBase):
Returns: Returns:
List[Branch] -- the branches on the stream List[Branch] -- the branches on the stream
""" """
metrics.track(metrics.BRANCH, self.account, {"name": "get"})
query = gql( query = gql(
""" """
query BranchesGet( query BranchesGet($stream_id: String!, $branches_limit: Int!, $commits_limit: Int!) {
$stream_id: String!,
$branches_limit: Int!,
$commits_limit: Int!
) {
stream(id: $stream_id) { stream(id: $stream_id) {
branches(limit: $branches_limit) { branches(limit: $branches_limit) {
items { items {
@@ -167,13 +150,8 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "branches", "items"] query=query, params=params, return_type=["stream", "branches", "items"]
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update( def update(
self, self, stream_id: str, branch_id: str, name: str = None, description: str = None
stream_id: str,
branch_id: str,
name: Optional[str] = None,
description: Optional[str] = None,
): ):
"""Update a branch """Update a branch
@@ -186,6 +164,7 @@ class Resource(ResourceBase):
Returns: Returns:
bool -- True if update is successful bool -- True if update is successful
""" """
metrics.track(metrics.BRANCH, self.account, {"name": "update"})
query = gql( query = gql(
""" """
mutation BranchUpdate($branch: BranchUpdateInput!) { mutation BranchUpdate($branch: BranchUpdateInput!) {
@@ -209,7 +188,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="branchUpdate", parse_response=False query=query, params=params, return_type="branchUpdate", parse_response=False
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, branch_id: str): def delete(self, stream_id: str, branch_id: str):
"""Delete a branch """Delete a branch
@@ -220,6 +198,7 @@ class Resource(ResourceBase):
Returns: Returns:
bool -- True if deletion is successful bool -- True if deletion is successful
""" """
metrics.track(metrics.BRANCH, self.account, {"name": "delete"})
query = gql( query = gql(
""" """
mutation BranchDelete($branch: BranchDeleteInput!) { mutation BranchDelete($branch: BranchDeleteInput!) {
@@ -1,24 +1,15 @@
from typing import List, Optional, Union from typing import Optional, List
from deprecated import deprecated
from gql import gql from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.api.models import Commit
from specklepy.logging import metrics
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
Commit,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException
NAME = "commit" NAME = "commit"
class Resource(ResourceBase): class Resource(ResourceBase):
""" """API Access class for commits"""
API Access class for commits
Commit resource is deprecated, please use version resource instead
"""
def __init__(self, account, basepath, client) -> None: def __init__(self, account, basepath, client) -> None:
super().__init__( super().__init__(
@@ -29,7 +20,6 @@ class Resource(ResourceBase):
) )
self.schema = Commit self.schema = Commit
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, commit_id: str) -> Commit: def get(self, stream_id: str, commit_id: str) -> Commit:
""" """
Gets a commit given a stream and the commit id Gets a commit given a stream and the commit id
@@ -68,7 +58,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "commit"] query=query, params=params, return_type=["stream", "commit"]
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, limit: int = 10) -> List[Commit]: def list(self, stream_id: str, limit: int = 10) -> List[Commit]:
""" """
Get a list of commits on a given stream Get a list of commits on a given stream
@@ -80,6 +69,7 @@ class Resource(ResourceBase):
Returns: Returns:
List[Commit] -- a list of the most recent commit objects List[Commit] -- a list of the most recent commit objects
""" """
metrics.track(metrics.COMMIT, self.account, {"name": "get"})
query = gql( query = gql(
""" """
query Commits($stream_id: String!, $limit: Int!) { query Commits($stream_id: String!, $limit: Int!) {
@@ -110,7 +100,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["stream", "commits", "items"] query=query, params=params, return_type=["stream", "commits", "items"]
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create( def create(
self, self,
stream_id: str, stream_id: str,
@@ -118,30 +107,26 @@ class Resource(ResourceBase):
branch_name: str = "main", branch_name: str = "main",
message: str = "", message: str = "",
source_application: str = "python", source_application: str = "python",
parents: Optional[List[str]] = None, parents: List[str] = None,
) -> Union[str, SpeckleException]: ) -> str:
""" """
Creates a commit on a branch Creates a commit on a branch
Arguments: Arguments:
stream_id {str} -- the stream you want to commit to stream_id {str} -- the stream you want to commit to
object_id {str} -- the hash of your commit object object_id {str} -- the hash of your commit object
branch_name {str} branch_name {str} -- the name of the branch to commit to (defaults to "main")
-- the name of the branch to commit to (defaults to "main") message {str} -- optional: a message to give more information about the commit
message {str} source_application{str} -- optional: the application from which the commit was created (defaults to "python")
-- optional: a message to give more information about the commit
source_application{str}
-- optional: the application from which the commit was created
(defaults to "python")
parents {List[str]} -- optional: the id of the parent commits parents {List[str]} -- optional: the id of the parent commits
Returns: Returns:
str -- the id of the created commit str -- the id of the created commit
""" """
metrics.track(metrics.COMMIT, self.account, {"name": "create"})
query = gql( query = gql(
""" """
mutation CommitCreate ($commit: CommitCreateInput!) mutation CommitCreate ($commit: CommitCreateInput!){ commitCreate(commit: $commit)}
{ commitCreate(commit: $commit)}
""" """
) )
params = { params = {
@@ -160,24 +145,22 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitCreate", parse_response=False query=query, params=params, return_type="commitCreate", parse_response=False
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(self, stream_id: str, commit_id: str, message: str) -> bool: def update(self, stream_id: str, commit_id: str, message: str) -> bool:
""" """
Update a commit Update a commit
Arguments: Arguments:
stream_id {str} stream_id {str} -- the id of the stream that contains the commit you'd like to update
-- the id of the stream that contains the commit you'd like to update
commit_id {str} -- the id of the commit you'd like to update commit_id {str} -- the id of the commit you'd like to update
message {str} -- the updated commit message message {str} -- the updated commit message
Returns: Returns:
bool -- True if the operation succeeded bool -- True if the operation succeeded
""" """
metrics.track(metrics.COMMIT, self.account, {"name": "update"})
query = gql( query = gql(
""" """
mutation CommitUpdate($commit: CommitUpdateInput!) mutation CommitUpdate($commit: CommitUpdateInput!){ commitUpdate(commit: $commit)}
{ commitUpdate(commit: $commit)}
""" """
) )
params = { params = {
@@ -188,23 +171,21 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitUpdate", parse_response=False query=query, params=params, return_type="commitUpdate", parse_response=False
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, commit_id: str) -> bool: def delete(self, stream_id: str, commit_id: str) -> bool:
""" """
Delete a commit Delete a commit
Arguments: Arguments:
stream_id {str} stream_id {str} -- the id of the stream that contains the commit you'd like to delete
-- the id of the stream that contains the commit you'd like to delete
commit_id {str} -- the id of the commit you'd like to delete commit_id {str} -- the id of the commit you'd like to delete
Returns: Returns:
bool -- True if the operation succeeded bool -- True if the operation succeeded
""" """
metrics.track(metrics.COMMIT, self.account, {"name": "delete"})
query = gql( query = gql(
""" """
mutation CommitDelete($commit: CommitDeleteInput!) mutation CommitDelete($commit: CommitDeleteInput!){ commitDelete(commit: $commit)}
{ commitDelete(commit: $commit)}
""" """
) )
params = {"commit": {"streamId": stream_id, "id": commit_id}} params = {"commit": {"streamId": stream_id, "id": commit_id}}
@@ -213,7 +194,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="commitDelete", parse_response=False query=query, params=params, return_type="commitDelete", parse_response=False
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def received( def received(
self, self,
stream_id: str, stream_id: str,
@@ -224,6 +204,7 @@ class Resource(ResourceBase):
""" """
Mark a commit object a received by the source application. Mark a commit object a received by the source application.
""" """
metrics.track(metrics.COMMIT, self.account, {"name": "received"})
query = gql( query = gql(
""" """
mutation CommitReceive($receivedInput:CommitReceivedInput!){ mutation CommitReceive($receivedInput:CommitReceivedInput!){
@@ -1,8 +1,6 @@
from typing import Dict, List from typing import Dict, List
from gql import gql from gql import gql
from specklepy.api.resource import ResourceBase
from specklepy.core.api.resource import ResourceBase
from specklepy.objects.base import Base from specklepy.objects.base import Base
NAME = "object" NAME = "object"
@@ -61,28 +59,21 @@ class Resource(ResourceBase):
""" """
Not advised - generally, you want to use `operations.send()`. Not advised - generally, you want to use `operations.send()`.
Create a new object on a stream. Create a new object on a stream. To send a base object, you can prepare it by running it through the
To send a base object, you can prepare it by running it through the `BaseObjectSerializer.traverse_base()` function to get a valid (serialisable) object to send.
`BaseObjectSerializer.traverse_base()` function to get a valid (serialisable)
object to send.
NOTE: this does not create a commit - you can create one with NOTE: this does not create a commit - you can create one with `SpeckleClient.commit.create`. Dynamic fields will be located in the 'data' dict of the received `Base` object
`SpeckleClient.commit.create`.
Dynamic fields will be located in the 'data' dict of the received `Base` object
Arguments: Arguments:
stream_id {str} -- the id of the stream you want to send the object to stream_id {str} -- the id of the stream you want to send the object to
objects {List[Dict]} objects {List[Dict]} -- a list of base dictionary objects (NOTE: must be json serialisable)
-- a list of base dictionary objects (NOTE: must be json serialisable)
Returns: Returns:
str -- the id of the object str -- the id of the object
""" """
query = gql( query = gql(
""" """
mutation ObjectCreate($object_input: ObjectCreateInput!) { mutation ObjectCreate($object_input: ObjectCreateInput!) { objectCreate(objectInput: $object_input) }
objectCreate(objectInput: $object_input)
}
""" """
) )
params = {"object_input": {"streamId": stream_id, "objects": objects}} params = {"object_input": {"streamId": stream_id, "objects": objects}}
@@ -1,17 +1,16 @@
import re import re
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
import requests
from gql import gql from gql import gql
from specklepy.api.models import ServerInfo
from specklepy.core.api.models import ServerInfo from specklepy.api.resource import ResourceBase
from specklepy.core.api.resource import ResourceBase from specklepy.logging import metrics
from specklepy.logging.exceptions import GraphQLException from specklepy.logging.exceptions import GraphQLException
NAME = "server" NAME = "server"
class ServerResource(ResourceBase): class Resource(ResourceBase):
"""API Access class for the server""" """API Access class for the server"""
def __init__(self, account, basepath, client) -> None: def __init__(self, account, basepath, client) -> None:
@@ -28,6 +27,7 @@ class ServerResource(ResourceBase):
Returns: Returns:
dict -- the server info in dictionary form dict -- the server info in dictionary form
""" """
metrics.track(metrics.SERVER, self.account, {"name": "get"})
query = gql( query = gql(
""" """
query Server { query Server {
@@ -57,28 +57,16 @@ class ServerResource(ResourceBase):
""" """
) )
server_info = self.make_request( return self.make_request(
query=query, return_type="serverInfo", schema=ServerInfo query=query, return_type="serverInfo", schema=ServerInfo
) )
if isinstance(server_info, ServerInfo) and isinstance(
server_info.canonicalUrl, str
):
r = requests.get(
server_info.canonicalUrl, headers={"User-Agent": "specklepy SDK"}
)
if "x-speckle-frontend-2" in r.headers:
server_info.frontend2 = True
else:
server_info.frontend2 = False
return server_info
def version(self) -> Tuple[Any, ...]: def version(self) -> Tuple[Any, ...]:
"""Get the server version """Get the server version
Returns: Returns:
the server version in the format (major, minor, patch, (tag, build)) tuple -- 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 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 = gql(
@@ -112,6 +100,7 @@ class ServerResource(ResourceBase):
Returns: Returns:
dict -- a dictionary of apps registered on the server dict -- a dictionary of apps registered on the server
""" """
metrics.track(metrics.SERVER, self.account, {"name": "apps"})
query = gql( query = gql(
""" """
query Apps { query Apps {
@@ -145,6 +134,7 @@ class ServerResource(ResourceBase):
Returns: Returns:
str -- the new API token. note: this is the only time you'll see the token! str -- the new API token. note: this is the only time you'll see the token!
""" """
metrics.track(metrics.SERVER, self.account, {"name": "create_token"})
query = gql( query = gql(
""" """
mutation TokenCreate($token: ApiTokenCreateInput!) { mutation TokenCreate($token: ApiTokenCreateInput!) {
@@ -170,6 +160,7 @@ class ServerResource(ResourceBase):
Returns: Returns:
bool -- True if the token was successfully deleted bool -- True if the token was successfully deleted
""" """
metrics.track(metrics.SERVER, self.account, {"name": "revoke_token"})
query = gql( query = gql(
""" """
mutation TokenRevoke($token: String!) { mutation TokenRevoke($token: String!) {
@@ -1,29 +1,18 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import List, Optional from typing import List, Optional
from deprecated import deprecated from deprecated import deprecated
from gql import gql from gql import gql
from specklepy.logging import metrics
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, Stream
from specklepy.api.resource import ResourceBase
from specklepy.logging.exceptions import UnsupportedException, SpeckleException
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
Stream,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException, UnsupportedException
NAME = "stream" NAME = "stream"
class Resource(ResourceBase): class Resource(ResourceBase):
""" """API Access class for streams"""
API Access class for streams
Stream resource is deprecated, please use project resource instead
"""
def __init__(self, account, basepath, client, server_version) -> None: def __init__(self, account, basepath, client, server_version) -> None:
super().__init__( super().__init__(
@@ -36,7 +25,6 @@ class Resource(ResourceBase):
self.schema = Stream self.schema = Stream
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream: def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream:
"""Get the specified stream from the server """Get the specified stream from the server
@@ -48,6 +36,7 @@ class Resource(ResourceBase):
Returns: Returns:
Stream -- the retrieved stream Stream -- the retrieved stream
""" """
metrics.track(metrics.STREAM, self.account, {"name": "get"})
query = gql( query = gql(
""" """
query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) { query Stream($id: String!, $branch_limit: Int!, $commit_limit: Int!) {
@@ -98,7 +87,6 @@ class Resource(ResourceBase):
return self.make_request(query=query, params=params, return_type="stream") return self.make_request(query=query, params=params, return_type="stream")
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_limit: int = 10) -> List[Stream]: def list(self, stream_limit: int = 10) -> List[Stream]:
"""Get a list of the user's streams """Get a list of the user's streams
@@ -108,6 +96,7 @@ class Resource(ResourceBase):
Returns: Returns:
List[Stream] -- A list of Stream objects List[Stream] -- A list of Stream objects
""" """
metrics.track(metrics.STREAM, self.account, {"name": "get"})
query = gql( query = gql(
""" """
query User($stream_limit: Int!) { query User($stream_limit: Int!) {
@@ -152,7 +141,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["user", "streams", "items"] query=query, params=params, return_type=["user", "streams", "items"]
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create( def create(
self, self,
name: str = "Anonymous Python Stream", name: str = "Anonymous Python Stream",
@@ -164,12 +152,12 @@ class Resource(ResourceBase):
Arguments: Arguments:
name {str} -- the name of the string name {str} -- the name of the string
description {str} -- a short description of the stream description {str} -- a short description of the stream
is_public {bool} is_public {bool} -- whether or not the stream can be viewed by anyone with the id
-- whether or not the stream can be viewed by anyone with the id
Returns: Returns:
id {str} -- the id of the newly created stream id {str} -- the id of the newly created stream
""" """
metrics.track(metrics.STREAM, self.account, {"name": "create"})
query = gql( query = gql(
""" """
mutation StreamCreate($stream: StreamCreateInput!) { mutation StreamCreate($stream: StreamCreateInput!) {
@@ -177,8 +165,7 @@ class Resource(ResourceBase):
} }
""" """
) )
if len(name) < 3 and len(name) != 0:
return SpeckleException(message="Stream Name must be at least 3 characters")
params = { params = {
"stream": {"name": name, "description": description, "isPublic": is_public} "stream": {"name": name, "description": description, "isPublic": is_public}
} }
@@ -187,13 +174,8 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamCreate", parse_response=False query=query, params=params, return_type="streamCreate", parse_response=False
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update( def update(
self, self, id: str, name: str = None, description: str = None, is_public: bool = None
id: str,
name: Optional[str] = None,
description: Optional[str] = None,
is_public: Optional[bool] = None,
) -> bool: ) -> bool:
"""Update an existing stream """Update an existing stream
@@ -201,12 +183,12 @@ class Resource(ResourceBase):
id {str} -- the id of the stream to be updated id {str} -- the id of the stream to be updated
name {str} -- the name of the string name {str} -- the name of the string
description {str} -- a short description of the stream description {str} -- a short description of the stream
is_public {bool} is_public {bool} -- whether or not the stream can be viewed by anyone with the id
-- whether or not the stream can be viewed by anyone with the id
Returns: Returns:
bool -- whether the stream update was successful bool -- whether the stream update was successful
""" """
metrics.track(metrics.STREAM, self.account, {"name": "update"})
query = gql( query = gql(
""" """
mutation StreamUpdate($stream: StreamUpdateInput!) { mutation StreamUpdate($stream: StreamUpdateInput!) {
@@ -228,7 +210,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamUpdate", parse_response=False query=query, params=params, return_type="streamUpdate", parse_response=False
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, id: str) -> bool: def delete(self, id: str) -> bool:
"""Delete a stream given its id """Delete a stream given its id
@@ -238,6 +219,7 @@ class Resource(ResourceBase):
Returns: Returns:
bool -- whether the deletion was successful bool -- whether the deletion was successful
""" """
metrics.track(metrics.STREAM, self.account, {"name": "delete"})
query = gql( query = gql(
""" """
mutation StreamDelete($id: String!) { mutation StreamDelete($id: String!) {
@@ -252,7 +234,6 @@ class Resource(ResourceBase):
query=query, params=params, return_type="streamDelete", parse_response=False query=query, params=params, return_type="streamDelete", parse_response=False
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def search( def search(
self, self,
search_query: str, search_query: str,
@@ -271,14 +252,10 @@ class Resource(ResourceBase):
Returns: Returns:
List[Stream] -- a list of Streams that match the search query List[Stream] -- a list of Streams that match the search query
""" """
metrics.track(metrics.STREAM, self.account, {"name": "search"})
query = gql( query = gql(
""" """
query StreamSearch( query StreamSearch($search_query: String!,$limit: Int!, $branch_limit:Int!, $commit_limit:Int!) {
$search_query: String!,
$limit: Int!,
$branch_limit:Int!,
$commit_limit:Int!
) {
streams(query: $search_query, limit: $limit) { streams(query: $search_query, limit: $limit) {
items { items {
id id
@@ -332,18 +309,17 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["streams", "items"] query=query, params=params, return_type=["streams", "items"]
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def favorite(self, stream_id: str, favorited: bool = True): def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream. """Favorite or unfavorite the given stream.
Arguments: Arguments:
stream_id {str} -- the id of the stream to favorite / unfavorite stream_id {str} -- the id of the stream to favorite / unfavorite
favorited {bool} favorited {bool} -- whether to favorite (True) or unfavorite (False) the stream
-- whether to favorite (True) or unfavorite (False) the stream
Returns: Returns:
Stream -- the stream with its `id`, `name`, and `favoritedDate` Stream -- the stream with its `id`, `name`, and `favoritedDate`
""" """
metrics.track(metrics.STREAM, self.account, {"name": "favorite"})
query = gql( query = gql(
""" """
mutation StreamFavorite($stream_id: String!, $favorited: Boolean!) { mutation StreamFavorite($stream_id: String!, $favorited: Boolean!) {
@@ -366,7 +342,59 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["streamFavorite"] query=query, params=params, return_type=["streamFavorite"]
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION) @deprecated(
version="2.6.4",
reason=(
"As of Speckle Server v2.6.4, this method is deprecated. "
"Users need to be invited and accept the invite before being added to a stream"
),
)
def grant_permission(self, stream_id: str, user_id: str, role: str):
"""Grant permissions to a user on a given stream
Valid for Speckle Server version < 2.6.4
Arguments:
stream_id {str} -- the id of the stream to grant permissions to
user_id {str} -- the id of the user to grant permissions for
role {str} -- the role to grant the user
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.PERMISSION, self.account, {"name": "add", "role": role})
if self.server_version and self.server_version >= (2, 6, 4):
raise UnsupportedException(
(
"Server mutation `grant_permission` is no longer supported as of Speckle Server v2.6.4. "
"Please use the new `update_permission` method to change an existing user's permission "
"or use the `invite` method to invite a user to a stream."
)
)
query = gql(
"""
mutation StreamGrantPermission($permission_params: StreamGrantPermissionInput !) {
streamGrantPermission(permissionParams: $permission_params)
}
"""
)
params = {
"permission_params": {
"streamId": stream_id,
"userId": user_id,
"role": role,
}
}
return self.make_request(
query=query,
params=params,
return_type="streamGrantPermission",
parse_response=False,
)
def get_all_pending_invites( def get_all_pending_invites(
self, stream_id: str self, stream_id: str
) -> List[PendingStreamCollaborator]: ) -> List[PendingStreamCollaborator]:
@@ -379,9 +407,9 @@ class Resource(ResourceBase):
stream_id {str} -- the stream id from which to get the pending invites stream_id {str} -- the stream id from which to get the pending invites
Returns: Returns:
List[PendingStreamCollaborator] List[PendingStreamCollaborator] -- a list of pending invites for the specified stream
-- a list of pending invites for the specified stream
""" """
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported() self._check_invites_supported()
query = gql( query = gql(
@@ -394,27 +422,19 @@ class Resource(ResourceBase):
inviteId inviteId
streamId streamId
streamName streamName
projectName
projectId
title title
role role
invitedBy{ invitedBy{
id id
name name
bio
company company
avatar avatar
verified
role
} }
user { user {
id id
name name
bio
company company
avatar avatar
verified
role
} }
} }
} }
@@ -430,14 +450,13 @@ class Resource(ResourceBase):
schema=PendingStreamCollaborator, schema=PendingStreamCollaborator,
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite( def invite(
self, self,
stream_id: str, stream_id: str,
email: Optional[str] = None, email: str = None,
user_id: Optional[str] = None, user_id: str = None,
role: str = "stream:contributor", # should default be reviewer? role: str = "stream:contributor", # should default be reviewer?
message: Optional[str] = None, message: str = None,
): ):
"""Invite someone to a stream using either their email or user id """Invite someone to a stream using either their email or user id
@@ -447,20 +466,18 @@ class Resource(ResourceBase):
stream_id {str} -- the id of the stream to invite the user to stream_id {str} -- the id of the stream to invite the user to
email {str} -- the email of the user to invite (use this OR `user_id`) email {str} -- the email of the user to invite (use this OR `user_id`)
user_id {str} -- the id of the user to invite (use this OR `email`) user_id {str} -- the id of the user to invite (use this OR `email`)
role {str} role {str} -- the role to assing to the user (defaults to `stream:contributor`)
-- the role to assign to the user (defaults to `stream:contributor`) message {str} -- a message to send along with this invite to the specified user
message {str}
-- a message to send along with this invite to the specified user
Returns: Returns:
bool -- True if the operation was successful bool -- True if the operation was successful
""" """
metrics.track(metrics.INVITE, self.account, {"name": "create"})
self._check_invites_supported() self._check_invites_supported()
if email is None and user_id is None: if email is None and user_id is None:
raise SpeckleException( raise SpeckleException(
"You must provide either an email or a user id to use the" "You must provide either an email or a user id to use the `stream.invite` method"
" `stream.invite` method"
) )
query = gql( query = gql(
@@ -487,13 +504,12 @@ class Resource(ResourceBase):
parse_response=False, parse_response=False,
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_batch( def invite_batch(
self, self,
stream_id: str, stream_id: str,
emails: Optional[List[str]] = None, emails: List[str] = None,
user_ids: Optional[List[None]] = None, user_ids: List[None] = None,
message: Optional[str] = None, message: str = None,
) -> bool: ) -> bool:
"""Invite a batch of users to a specified stream. """Invite a batch of users to a specified stream.
@@ -501,21 +517,18 @@ class Resource(ResourceBase):
Arguments: Arguments:
stream_id {str} -- the id of the stream to invite the user to stream_id {str} -- the id of the stream to invite the user to
emails {List[str]} emails {List[str]} -- the email of the user to invite (use this and/or `user_ids`)
-- the email of the user to invite (use this and/or `user_ids`) user_id {List[str]} -- the id of the user to invite (use this and/or `emails`)
user_id {List[str]} message {str} -- a message to send along with this invite to the specified user
-- the id of the user to invite (use this and/or `emails`)
message {str}
-- a message to send along with this invite to the specified user
Returns: Returns:
bool -- True if the operation was successful bool -- True if the operation was successful
""" """
metrics.track(metrics.INVITE, self.account, {"name": "batch create"})
self._check_invites_supported() self._check_invites_supported()
if emails is None and user_ids is None: if emails is None and user_ids is None:
raise SpeckleException( raise SpeckleException(
"You must provide either an email or a user id to use the" "You must provide either an email or a user id to use the `stream.invite` method"
" `stream.invite` method"
) )
query = gql( query = gql(
@@ -528,14 +541,14 @@ class Resource(ResourceBase):
email_invites = [ email_invites = [
{"streamId": stream_id, "message": message, "email": email} {"streamId": stream_id, "message": message, "email": email}
for email in (emails if emails is not None else []) for email in emails
if email is not None if emails is not None
] ]
user_invites = [ user_invites = [
{"streamId": stream_id, "message": message, "userId": user_id} {"streamId": stream_id, "message": message, "userId": user_id}
for user_id in (user_ids if user_ids is not None else []) for user_id in user_ids
if user_id is not None if user_ids is not None
] ]
params = {"input": [*email_invites, *user_invites]} params = {"input": [*email_invites, *user_invites]}
@@ -547,7 +560,6 @@ class Resource(ResourceBase):
parse_response=False, parse_response=False,
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool: def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite """Cancel an existing stream invite
@@ -560,6 +572,7 @@ class Resource(ResourceBase):
Returns: Returns:
bool -- true if the operation was successful bool -- true if the operation was successful
""" """
metrics.track(metrics.INVITE, self.account, {"name": "cancel"})
self._check_invites_supported() self._check_invites_supported()
query = gql( query = gql(
@@ -579,30 +592,25 @@ class Resource(ResourceBase):
parse_response=False, parse_response=False,
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool: def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite """Accept or decline a stream invite
Requires Speckle Server version >= 2.6.4 Requires Speckle Server version >= 2.6.4
Arguments: Arguments:
stream_id {str} stream_id {str} -- the id of the stream for which the user has a pending invite
-- the id of the stream for which the user has a pending invite
token {str} -- the token of the invite to use token {str} -- the token of the invite to use
accept {bool} -- whether or not to accept the invite (defaults to True) accept {bool} -- whether or not to accept the invite (defaults to True)
Returns: Returns:
bool -- true if the operation was successful bool -- true if the operation was successful
""" """
metrics.track(metrics.INVITE, self.account, {"name": "use"})
self._check_invites_supported() self._check_invites_supported()
query = gql( query = gql(
""" """
mutation StreamInviteUse( mutation StreamInviteUse($accept: Boolean!, $streamId: String!, $token: String!) {
$accept: Boolean!,
$streamId: String!,
$token: String!
) {
streamInviteUse(accept: $accept, streamId: $streamId, token: $token) streamInviteUse(accept: $accept, streamId: $streamId, token: $token)
} }
""" """
@@ -617,7 +625,6 @@ class Resource(ResourceBase):
parse_response=False, parse_response=False,
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update_permission(self, stream_id: str, user_id: str, role: str): def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream """Updates permissions for a user on a given stream
@@ -631,19 +638,19 @@ class Resource(ResourceBase):
Returns: Returns:
bool -- True if the operation was successful bool -- True if the operation was successful
""" """
if self.server_version and ( metrics.track(
self.server_version != ("dev",) and self.server_version < (2, 6, 4) metrics.PERMISSION, self.account, {"name": "update", "role": role}
): )
if self.server_version and self.server_version < (2, 6, 4):
raise UnsupportedException( raise UnsupportedException(
"Server mutation `update_permission` is only supported as of Speckle" (
" Server v2.6.4. Please update your Speckle Server to use this method" "Server mutation `update_permission` is only supported as of Speckle Server v2.6.4. "
" or use the `grant_permission` method instead." "Please update your Speckle Server to use this method or use the `grant_permission` method instead."
)
) )
query = gql( query = gql(
""" """
mutation StreamUpdatePermission( mutation StreamUpdatePermission($permission_params: StreamUpdatePermissionInput !) {
$permission_params: StreamUpdatePermissionInput!
) {
streamUpdatePermission(permissionParams: $permission_params) streamUpdatePermission(permissionParams: $permission_params)
} }
""" """
@@ -664,7 +671,6 @@ class Resource(ResourceBase):
parse_response=False, parse_response=False,
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def revoke_permission(self, stream_id: str, user_id: str): def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream """Revoke permissions from a user on a given stream
@@ -675,11 +681,10 @@ class Resource(ResourceBase):
Returns: Returns:
bool -- True if the operation was successful bool -- True if the operation was successful
""" """
metrics.track(metrics.PERMISSION, self.account, {"name": "revoke"})
query = gql( query = gql(
""" """
mutation StreamRevokePermission( mutation StreamRevokePermission($permission_params: StreamRevokePermissionInput !) {
$permission_params: StreamRevokePermissionInput!
) {
streamRevokePermission(permissionParams: $permission_params) streamRevokePermission(permissionParams: $permission_params)
} }
""" """
@@ -694,52 +699,32 @@ class Resource(ResourceBase):
parse_response=False, parse_response=False,
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity( def activity(
self, self,
stream_id: str, stream_id: str,
action_type: Optional[str] = None, action_type: str = None,
limit: int = 20, limit: int = 20,
before: Optional[datetime] = None, before: datetime = None,
after: Optional[datetime] = None, after: datetime = None,
cursor: Optional[datetime] = None, cursor: datetime = None,
): ):
""" """
Get the activity from a given stream in an Activity collection. Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of any tz Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
as they will be converted to UTC ISO format strings
stream_id {str} -- the id of the stream to get activity from stream_id {str} -- the id of the stream to get activity from
action_type {str} action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
-- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return limit {int} -- max number of Activity items to return
before {datetime} before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
-- latest cutoff for activity (ie: return all activity _before_ this time) after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination cursor {datetime} -- timestamp cursor for pagination
""" """
query = gql( query = gql(
""" """
query StreamActivity( query StreamActivity($stream_id: String!, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
$stream_id: String!,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
stream(id: $stream_id) { stream(id: $stream_id) {
activity( activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount totalCount
cursor cursor
items { items {
@@ -762,19 +747,18 @@ class Resource(ResourceBase):
"stream_id": stream_id, "stream_id": stream_id,
"limit": limit, "limit": limit,
"action_type": action_type, "action_type": action_type,
"before": ( "before": before.astimezone(timezone.utc).isoformat()
before.astimezone(timezone.utc).isoformat() if before else before if before
), else before,
"after": after.astimezone(timezone.utc).isoformat() if after else after, "after": after.astimezone(timezone.utc).isoformat() if after else after,
"cursor": ( "cursor": cursor.astimezone(timezone.utc).isoformat()
cursor.astimezone(timezone.utc).isoformat() if cursor else cursor if cursor
), else cursor,
} }
except AttributeError as e: except AttributeError as e:
raise SpeckleException( raise SpeckleException(
"Could not get stream activity - `before`, `after`, and `cursor` must" "Could not get stream activity - `before`, `after`, and `cursor` must be in `datetime` format if provided",
" be in `datetime` format if provided", ValueError,
ValueError(),
) from e ) from e
return self.make_request( return self.make_request(
@@ -1,16 +1,9 @@
from typing import Callable, Dict, List, Union
from functools import wraps from functools import wraps
from typing import Callable, Dict, List, Optional, Union
from deprecated import deprecated
from gql import gql from gql import gql
from graphql import DocumentNode from graphql import DocumentNode
from specklepy.api.resource import ResourceBase
from specklepy.core.api.models.deprecated import ( from specklepy.api.resources.stream import Stream
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
Stream,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException from specklepy.logging.exceptions import SpeckleException
NAME = "subscribe" NAME = "subscribe"
@@ -40,15 +33,12 @@ class Resource(ResourceBase):
name=NAME, name=NAME,
) )
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient @check_wsclient
async def stream_added(self, callback: Optional[Callable] = None): async def stream_added(self, callback: Callable = None):
"""Subscribes to new stream added event for your profile. """Subscribes to new stream added event for your profile. Use this to display an up-to-date list of streams.
Use this to display an up-to-date list of streams.
Arguments: Arguments:
callback {Callable[Stream]} -- a function that takes the updated stream callback {Callable[Stream]} -- a function that takes the updated stream as an argument and executes each time a stream is added
as an argument and executes each time a stream is added
Returns: Returns:
Stream -- the update stream Stream -- the update stream
@@ -63,16 +53,12 @@ class Resource(ResourceBase):
) )
@check_wsclient @check_wsclient
async def stream_updated(self, id: str, callback: Optional[Callable] = None): async def stream_updated(self, id: str, callback: Callable = None):
""" """Subscribes to stream updated event. Use this in clients/components that pertain only to this stream.
Subscribes to stream updated event.
Use this in clients/components that pertain only to this stream.
Arguments: Arguments:
id {str} -- the stream id of the stream to subscribe to id {str} -- the stream id of the stream to subscribe to
callback {Callable[Stream]} callback {Callable[Stream]} -- a function that takes the updated stream as an argument and executes each time the stream is updated
-- a function that takes the updated stream
as an argument and executes each time the stream is updated
Returns: Returns:
Stream -- the update stream Stream -- the update stream
@@ -93,17 +79,11 @@ class Resource(ResourceBase):
) )
@check_wsclient @check_wsclient
async def stream_removed(self, callback: Optional[Callable] = None): async def stream_removed(self, callback: Callable = None):
"""Subscribes to stream removed event for your profile. """Subscribes to stream removed event for your profile. Use this to display an up-to-date list of streams for your profile. NOTE: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of revokedBy in the payload.
Use this to display an up-to-date list of streams for your profile.
NOTE: If someone revokes your permissions on a stream,
this subscription will be triggered with an extra value of revokedBy
in the payload.
Arguments: Arguments:
callback {Callable[Dict]} callback {Callable[Dict]} -- a function that takes the returned dict as an argument and executes each time a stream is removed
-- a function that takes the returned dict as an argument
and executes each time a stream is removed
Returns: Returns:
dict -- dict containing 'id' of stream removed and optionally 'revokedBy' dict -- dict containing 'id' of stream removed and optionally 'revokedBy'
@@ -125,9 +105,9 @@ class Resource(ResourceBase):
async def subscribe( async def subscribe(
self, self,
query: DocumentNode, query: DocumentNode,
params: Optional[Dict] = None, params: Dict = None,
callback: Optional[Callable] = None, callback: Callable = None,
return_type: Optional[Union[str, List]] = None, return_type: Union[str, List] = None,
schema=None, schema=None,
parse_response: bool = True, parse_response: bool = True,
): ):
@@ -1,25 +1,13 @@
from datetime import datetime, timezone
from typing import List, Optional, Union from typing import List, Optional, Union
from datetime import datetime, timezone
from deprecated import deprecated
from gql import gql from gql import gql
from specklepy.logging import metrics
from specklepy.core.api.models import (
ActivityCollection,
PendingStreamCollaborator,
User,
)
from specklepy.core.api.resource import ResourceBase
from specklepy.logging.exceptions import SpeckleException from specklepy.logging.exceptions import SpeckleException
from specklepy.api.resource import ResourceBase
from specklepy.api.models import ActivityCollection, PendingStreamCollaborator, User
NAME = "user" NAME = "user"
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = (
"The user resource is deprecated, please use the active_user or other_user"
" resources"
)
class Resource(ResourceBase): class Resource(ResourceBase):
"""API Access class for users""" """API Access class for users"""
@@ -34,12 +22,8 @@ class Resource(ResourceBase):
) )
self.schema = User self.schema = User
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT) def get(self, id: str = None) -> User:
def get(self, id: Optional[str] = None) -> User: """Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
"""
Gets the profile of a user.
If no id argument is provided, will return the current authenticated
user's profile (as extracted from the authorization header).
Arguments: Arguments:
id {str} -- the user id id {str} -- the user id
@@ -47,6 +31,7 @@ class Resource(ResourceBase):
Returns: Returns:
User -- the retrieved user User -- the retrieved user
""" """
metrics.track(metrics.USER, self.account, {"name": "get"})
query = gql( query = gql(
""" """
query User($id: String) { query User($id: String) {
@@ -69,13 +54,10 @@ class Resource(ResourceBase):
return self.make_request(query=query, params=params, return_type="user") return self.make_request(query=query, params=params, return_type="user")
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search( def search(
self, search_query: str, limit: int = 25 self, search_query: str, limit: int = 25
) -> Union[List[User], SpeckleException]: ) -> Union[List[User], SpeckleException]:
""" """Searches for user by name or email. The search query must be at least 3 characters long
Searches for user by name or email.
The search query must be at least 3 characters long
Arguments: Arguments:
search_query {str} -- a string to search for search_query {str} -- a string to search for
@@ -88,6 +70,7 @@ class Resource(ResourceBase):
message="User search query must be at least 3 characters" message="User search query must be at least 3 characters"
) )
metrics.track(metrics.USER, self.account, {"name": "search"})
query = gql( query = gql(
""" """
query UserSearch($search_query: String!, $limit: Int!) { query UserSearch($search_query: String!, $limit: Int!) {
@@ -110,13 +93,8 @@ class Resource(ResourceBase):
query=query, params=params, return_type=["userSearch", "items"] query=query, params=params, return_type=["userSearch", "items"]
) )
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def update( def update(
self, self, name: str = None, company: str = None, bio: str = None, avatar: str = None
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
): ):
"""Updates your user profile. All arguments are optional. """Updates your user profile. All arguments are optional.
@@ -129,6 +107,7 @@ class Resource(ResourceBase):
Returns: Returns:
bool -- True if your profile was updated successfully bool -- True if your profile was updated successfully
""" """
metrics.track(metrics.USER, self.account, {"name": "update"})
query = gql( query = gql(
""" """
mutation UserUpdate($user: UserUpdateInput!) { mutation UserUpdate($user: UserUpdateInput!) {
@@ -142,63 +121,41 @@ class Resource(ResourceBase):
if not params["user"]: if not params["user"]:
return SpeckleException( return SpeckleException(
message=( message="You must provide at least one field to update your user profile"
"You must provide at least one field to update your user profile"
)
) )
return self.make_request( return self.make_request(
query=query, params=params, return_type="userUpdate", parse_response=False query=query, params=params, return_type="userUpdate", parse_response=False
) )
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def activity( def activity(
self, self,
user_id: Optional[str] = None, user_id: str = None,
limit: int = 20, limit: int = 20,
action_type: Optional[str] = None, action_type: str = None,
before: Optional[datetime] = None, before: datetime = None,
after: Optional[datetime] = None, after: datetime = None,
cursor: Optional[datetime] = None, cursor: datetime = None,
): ):
""" """
Get the activity from a given stream in an Activity collection. Get the activity from a given stream in an Activity collection. Step into the activity `items` for the list of activity.
Step into the activity `items` for the list of activity. If no id argument is provided, will return the current authenticated user's activity (as extracted from the authorization header).
If no id argument is provided, will return the current authenticated
user's activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as Note: all timestamps arguments should be `datetime` of any tz as they will be converted to UTC ISO format strings
they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type action_type {str} -- filter results to a single action type (eg: `commit_create` or `commit_receive`)
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return limit {int} -- max number of Activity items to return
before {datetime} before {datetime} -- latest cutoff for activity (ie: return all activity _before_ this time)
-- latest cutoff for activity (ie: return all activity _before_ this time) after {datetime} -- oldest cutoff for activity (ie: return all activity _after_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination cursor {datetime} -- timestamp cursor for pagination
""" """
query = gql( query = gql(
""" """
query UserActivity( query UserActivity($user_id: String, $action_type: String, $before:DateTime, $after: DateTime, $cursor: DateTime, $limit: Int){
$user_id: String,
$action_type: String,
$before:DateTime,
$after: DateTime,
$cursor: DateTime,
$limit: Int
){
user(id: $user_id) { user(id: $user_id) {
activity( activity(actionType: $action_type, before: $before, after: $after, cursor: $cursor, limit: $limit) {
actionType: $action_type,
before: $before,
after: $after,
cursor: $cursor,
limit: $limit
) {
totalCount totalCount
cursor cursor
items { items {
@@ -233,16 +190,15 @@ class Resource(ResourceBase):
schema=ActivityCollection, schema=ActivityCollection,
) )
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]: def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites """Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4 Requires Speckle Server version >= 2.6.4
Returns: Returns:
List[PendingStreamCollaborator] List[PendingStreamCollaborator] -- a list of pending invites for the current user
-- a list of pending invites for the current user
""" """
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported() self._check_invites_supported()
query = gql( query = gql(
@@ -273,9 +229,8 @@ class Resource(ResourceBase):
schema=PendingStreamCollaborator, schema=PendingStreamCollaborator,
) )
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite( def get_pending_invite(
self, stream_id: str, token: Optional[str] = None self, stream_id: str, token: str = None
) -> Optional[PendingStreamCollaborator]: ) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream. """Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned. If no invite_id is provided, any valid invite will be returned.
@@ -287,9 +242,9 @@ class Resource(ResourceBase):
token {str} -- the token of the invite to look for (optional) token {str} -- the token of the invite to look for (optional)
Returns: Returns:
PendingStreamCollaborator PendingStreamCollaborator -- the invite for the given stream (or None if it isn't found)
-- the invite for the given stream (or None if it isn't found)
""" """
metrics.track(metrics.INVITE, self.account, {"name": "get"})
self._check_invites_supported() self._check_invites_supported()
query = gql( query = gql(
+640
View File
@@ -0,0 +1,640 @@
scalar DateTime
scalar EmailAddress
scalar BigInt
scalar JSONObject
directive @hasScope(scope: String!) on FIELD_DEFINITION
directive @hasRole(role: String!) on FIELD_DEFINITION
type Query {
"""
Stare into the void.
"""
_: String
}
type Mutation{
"""
The void stares back.
"""
_: String
}
type Subscription{
"""
It's lonely in the void.
"""
_: String
},extend type Query {
"""
Gets a specific app from the server.
"""
app( id: String! ): ServerApp
"""
Returns all the publicly available apps on this server.
"""
apps: [ServerAppListItem]
}
type ServerApp {
id: String!
secret: String!
name: String!
description: String
termsAndConditionsLink: String
logo: String
public: Boolean
trustByDefault: Boolean
author: AppAuthor
createdAt: DateTime!
redirectUrl: String!
scopes: [Scope]!
}
type ServerAppListItem {
id: String!
name: String!
description: String
termsAndConditionsLink: String
logo: String
author: AppAuthor
}
type AppAuthor {
name: String
id: String
}
extend type User {
"""
Returns the apps you have authorized.
"""
authorizedApps: [ServerAppListItem]
@hasRole(role: "server:user")
@hasScope(scope: "apps:read")
"""
Returns the apps you have created.
"""
createdApps: [ServerAppListItem]
@hasRole(role: "server:user")
@hasScope(scope: "apps:read")
}
extend type Mutation {
"""
Register a new third party application.
"""
appCreate(app: AppCreateInput!): String!
@hasRole(role: "server:user")
@hasScope(scope: "apps:write")
"""
Update an existing third party application. **Note: This will invalidate all existing tokens, refresh tokens and access codes and will require existing users to re-authorize it.**
"""
appUpdate(app: AppUpdateInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "apps:write")
"""
Deletes a thirty party application.
"""
appDelete(appId: String!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "apps:write")
"""
Revokes (de-authorizes) an application that you have previously authorized.
"""
appRevokeAccess(appId: String!): Boolean
@hasRole(role: "server:user")
@hasScope(scope: "apps:write")
}
input AppCreateInput {
name: String!
description: String!
termsAndConditionsLink: String
logo: String
public: Boolean
redirectUrl: String!
scopes: [String]!
}
input AppUpdateInput {
id: String!
name: String!
description: String!
termsAndConditionsLink: String
logo: String
public: Boolean
redirectUrl: String!
scopes: [String]!
}
,extend type ServerInfo {
"""
The authentication strategies available on this server.
"""
authStrategies: [AuthStrategy]
}
type AuthStrategy {
id: String!,
name: String!,
icon: String!,
url: String!,
color: String
}
,extend type User{
"""
Returns a list of your personal api tokens.
"""
apiTokens: [ApiToken]
@hasRole(role: "server:user")
@hasScope(scope: "tokens:read")
}
type ApiToken {
id: String!
name: String!
lastChars: String!
scopes: [String]!
createdAt: DateTime! #date
lifespan: BigInt!
lastUsed: String! #date
}
input ApiTokenCreateInput {
scopes: [String!]!,
name: String!,
lifespan: BigInt
}
extend type Mutation {
"""
Creates an personal api token.
"""
apiTokenCreate(token: ApiTokenCreateInput!):String!
@hasRole(role: "server:user")
@hasScope(scope: "tokens:write")
"""
Revokes (deletes) an personal api token.
"""
apiTokenRevoke(token: String!):Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "tokens:write")
}
,extend type Stream {
commits(limit: Int! = 25, cursor: String): CommitCollection
commit(id: String!): Commit
branches(limit: Int! = 25, cursor: String): BranchCollection
branch(name: String!): Branch
}
extend type User {
commits(limit: Int! = 25, cursor: String): CommitCollectionUser
}
type Branch {
id: String!
name: String!
author: User!
description: String
commits(limit: Int! = 25, cursor: String): CommitCollection
}
type Commit {
id: String!
referencedObject: String!
message: String
authorName: String
authorId: String
createdAt: DateTime
}
type CommitCollectionUserNode {
id: String!
referencedObject: String!
message: String
streamId: String
streamName: String
createdAt: DateTime
}
type BranchCollection {
totalCount: Int!
cursor: String
items: [Branch]
}
type CommitCollection {
totalCount: Int!
cursor: String
items: [Commit]
}
type CommitCollectionUser {
totalCount: Int!
cursor: String
items: [CommitCollectionUserNode]
}
extend type Mutation {
branchCreate(branch: BranchCreateInput!): String!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
branchUpdate(branch: BranchUpdateInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
branchDelete(branch: BranchDeleteInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
commitCreate(commit: CommitCreateInput!): String!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
commitUpdate(commit: CommitUpdateInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
commitDelete(commit: CommitDeleteInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
}
extend type Subscription {
# TODO: auth for these subscriptions
"""
Subscribe to branch created event
"""
branchCreated(streamId: String!): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to branch updated event.
"""
branchUpdated(streamId: String!, branchId: String): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to branch deleted event
"""
branchDeleted(streamId: String!): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to commit created event
"""
commitCreated(streamId: String!): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to commit updated event.
"""
commitUpdated(streamId: String!, commitId: String): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribe to commit deleted event
"""
commitDeleted(streamId: String!): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
}
input BranchCreateInput {
streamId: String!
name: String!
description: String
}
input BranchUpdateInput {
streamId: String!
id: String!
name: String
description: String
}
input BranchDeleteInput {
streamId: String!
id: String!
}
input CommitCreateInput {
streamId: String!
branchName: String!
objectId: String!
message: String
previousCommitIds: [String]
}
input CommitUpdateInput {
streamId: String!
id: String!
message: String!
}
input CommitDeleteInput {
streamId: String!
id: String!
}
,extend type Stream {
object( id: String! ): Object
}
type Object {
id: String!
speckleType: String!
applicationId: String
createdAt: DateTime
totalChildrenCount: Int
"""
The full object, with all its props & other things. **NOTE:** If you're requesting objects for the purpose of recreating & displaying, you probably only want to request this specific field.
"""
data: JSONObject
"""
Get any objects that this object references. In the case of commits, this will give you a commit's constituent objects.
**NOTE**: Providing any of the two last arguments ( `query`, `orderBy` ) will trigger a different code branch that executes a much more expensive SQL query. It is not recommended to do so for basic clients that are interested in purely getting all the objects of a given commit.
"""
children(
limit: Int! = 100,
depth: Int! = 50,
select: [String],
cursor: String,
query: [JSONObject!],
orderBy: JSONObject ): ObjectCollection!
}
type ObjectCollection {
totalCount: Int!
cursor: String
objects: [Object]!
}
extend type Mutation {
objectCreate( objectInput: ObjectCreateInput! ): [String]!
}
input ObjectCreateInput {
"""
The stream against which these objects will be created.
"""
streamId: String!
"""
The objects you want to create.
"""
objects: [JSONObject]!
},extend type Query {
serverInfo: ServerInfo!
}
"""
Information about this server.
"""
type ServerInfo {
name: String!
company: String
description: String
adminContact: String
canonicalUrl: String
termsOfService: String
roles: [Role]!
scopes: [Scope]!
}
"""
Available roles.
"""
type Role {
name: String!
description: String!
resourceTarget: String!
}
"""
Available scopes.
"""
type Scope {
name: String!
description: String!
}
extend type Mutation {
serverInfoUpdate(info: ServerInfoUpdateInput!): Boolean
@hasRole(role: "server:admin")
@hasScope(scope: "server:setup")
}
input ServerInfoUpdateInput {
name: String!
company: String
description: String
adminContact: String
termsOfService: String
}
,extend type Query {
"""
Returns a specific stream.
"""
stream( id: String! ): Stream
"""
All the streams of the current user, pass in the `query` parameter to search by name, description or ID.
"""
streams( query: String, limit: Int = 25, cursor: String ): StreamCollection
@hasScope(scope: "streams:read")
}
type Stream {
id: String!
name: String!
description: String
isPublic: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
collaborators: [ StreamCollaborator ]!
}
extend type User {
"""
All the streams that a user has access to.
"""
streams( limit: Int! = 25, cursor: String ): StreamCollection
}
type StreamCollaborator {
id: String!
name: String!
role: String!
company: String
avatar: String
}
type StreamCollection {
totalCount: Int!
cursor: String
items: [ Stream ]
}
extend type Mutation {
"""
Creates a new stream.
"""
streamCreate( stream: StreamCreateInput! ): String
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Updates an existing stream.
"""
streamUpdate( stream: StreamUpdateInput! ): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Deletes an existing stream.
"""
streamDelete( id: String! ): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Grants permissions to a user on a given stream.
"""
streamGrantPermission( permissionParams: StreamGrantPermissionInput! ): Boolean
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
"""
Revokes the permissions of a user on a given stream.
"""
streamRevokePermission( permissionParams: StreamRevokePermissionInput! ): Boolean
@hasRole(role: "server:user")
@hasScope(scope: "streams:write")
}
extend type Subscription {
#
# User bound subscriptions that operate on the stream collection of an user
# Example relevant view/usecase: updating the list of streams for a user.
#
"""
Subscribes to new stream added event for your profile. Use this to display an up-to-date list of streams.
**NOTE**: If someone shares a stream with you, this subscription will be triggered with an extra value of `sharedBy` in the payload.
"""
userStreamAdded: JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "profile:read")
"""
Subscribes to stream removed event for your profile. Use this to display an up-to-date list of streams for your profile.
**NOTE**: If someone revokes your permissions on a stream, this subscription will be triggered with an extra value of `revokedBy` in the payload.
"""
userStreamRemoved: JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "profile:read")
#
# Stream bound subscriptions that operate on the stream itself.
# Example relevant view/usecase: a single stream connector, or view, or component in a web app
#
"""
Subscribes to stream updated event. Use this in clients/components that pertain only to this stream.
"""
streamUpdated( streamId: String ): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
"""
Subscribes to stream deleted event. Use this in clients/components that pertain only to this stream.
"""
streamDeleted( streamId: String ): JSONObject
@hasRole(role: "server:user")
@hasScope(scope: "streams:read")
}
input StreamCreateInput {
name: String
description: String
isPublic: Boolean
}
input StreamUpdateInput {
id: String!
name: String
description: String
isPublic: Boolean
}
input StreamGrantPermissionInput {
streamId: String!,
userId: String!,
role: String!
}
input StreamRevokePermissionInput {
streamId: String!,
userId: String!
}
,extend type Query {
"""
Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
"""
user(id: String): User
userSearch(
query: String!
limit: Int! = 25
cursor: String
): UserSearchResultCollection
userPwdStrength(pwd: String!): JSONObject
}
"""
Base user type.
"""
type User {
id: String!
suuid: String
email: String
name: String
bio: String
company: String
avatar: String
verified: Boolean
profiles: JSONObject
role: String
}
type UserSearchResultCollection {
cursor: String
items: [UserSearchResult]
}
type UserSearchResult {
id: String!
name: String
bio: String
company: String
avatar: String
verified: Boolean
}
extend type Mutation {
"""
Edits a user's profile.
"""
userUpdate(user: UserUpdateInput!): Boolean!
}
input UserUpdateInput {
name: String
company: String
bio: String
}
+166
View File
@@ -0,0 +1,166 @@
from warnings import warn
from urllib.parse import urlparse, unquote
from specklepy.api.credentials import (
Account,
get_account_from_token,
get_local_accounts,
)
from specklepy.logging import metrics
from specklepy.api.client import SpeckleClient
from specklepy.transports.server.server import ServerTransport
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
class StreamWrapper:
"""
The `StreamWrapper` gives you some handy helpers to deal with urls and get authenticated clients and transports.
Construct a `StreamWrapper` with a stream, branch, commit, or object URL. The corresponding ids will be stored
in the wrapper. If you have local accounts on the machine, you can use the `get_account` and `get_client` methods
to get a local account for the server. You can also pass a token into `get_client` if you don't have a corresponding
local account for the server.
```py
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://speckle.xyz/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
# get an authenticated ServerTransport if you have a local account for the server
transport = wrapper.get_transport()
```
"""
stream_url: str = None
use_ssl: bool = True
host: str = None
stream_id: str = None
commit_id: str = None
object_id: str = None
branch_name: str = None
_client: SpeckleClient = None
_account: Account = None
def __repr__(self):
return f"StreamWrapper( server: {self.host}, stream_id: {self.stream_id}, type: {self.type} )"
def __str__(self) -> str:
return self.__repr__()
@property
def type(self) -> str:
if self.object_id:
return "object"
elif self.commit_id:
return "commit"
elif self.branch_name:
return "branch"
else:
return "stream" if self.stream_id else "invalid"
def __init__(self, url: str) -> None:
self.stream_url = url
parsed = urlparse(url)
self.host = parsed.netloc
self.use_ssl = parsed.scheme == "https"
segments = parsed.path.strip("/").split("/", 3)
metrics.track(metrics.STREAM_WRAPPER, self.get_account())
if not segments or len(segments) < 2:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
while segments:
segment = segments.pop(0)
if segments and segment.lower() == "streams":
self.stream_id = segments.pop(0)
elif segments and segment.lower() == "commits":
self.commit_id = segments.pop(0)
elif segments and segment.lower() == "branches":
self.branch_name = unquote(segments.pop(0))
elif segments and segment.lower() == "objects":
self.object_id = segments.pop(0)
elif segment.lower() == "globals":
self.branch_name = "globals"
if segments:
self.commit_id = segments.pop(0)
else:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - invalid URL provided."
)
if not self.stream_id:
raise SpeckleException(
f"Cannot parse {url} into a stream wrapper class - no stream id found."
)
@property
def server_url(self):
return f"{'https' if self.use_ssl else 'http'}://{self.host}"
def get_account(self, token: str = None) -> Account:
"""
Gets an account object for this server from the local accounts db (added via Speckle Manager or a json file)
"""
if self._account and self._account.token:
return self._account
self._account = next(
(a for a in get_local_accounts() if self.host in a.serverInfo.url),
None,
)
if not self._account:
self._account = get_account_from_token(token, self.server_url)
if self._client:
self._client.authenticate_with_account(self._account)
return self._account
def get_client(self, token: str = None) -> SpeckleClient:
"""
Gets an authenticated client for this server. You may provide a token if there aren't any local accounts on this
machine. If no account is found and no token is provided, an unauthenticated client is returned.
Arguments:
token {str} -- optional token if no local account is available (defaults to None)
Returns:
SpeckleClient -- authenticated with a corresponding local account or the provided token
"""
if self._client and token is None:
return self._client
if not self._account or not self._account.token:
self.get_account(token)
if not self._client:
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)
return self._client
if self._account.token:
self._client.authenticate_with_account(self._account)
else:
self._client.authenticate_with_token(token)
return self._client
def get_transport(self, token: str = None) -> ServerTransport:
"""
Gets a server transport for this stream using an authenticated client. If there is no local account for this
server and the client was not authenticated with a token, this will throw an exception.
Returns:
ServerTransport -- constructed for this stream with a pre-authenticated client
"""
if not self._account or not self._account.token:
self.get_account(token)
return ServerTransport(self.stream_id, account=self._account)
@@ -11,17 +11,6 @@ class SpeckleException(Exception):
return f"SpeckleException: {self.message}" return f"SpeckleException: {self.message}"
class SpeckleInvalidUnitException(SpeckleException):
def __init__(self, invalid_unit: Any) -> None:
super().__init__(
message=(
"Invalid units: expected type str but received"
f" {type(invalid_unit)} ({invalid_unit})."
),
exception=None,
)
class SerializationException(SpeckleException): class SerializationException(SpeckleException):
def __init__(self, message: str, obj: Any, exception: Exception = None) -> None: def __init__(self, message: str, obj: Any, exception: Exception = None) -> None:
super().__init__(message=message, exception=exception) super().__init__(message=message, exception=exception)
@@ -29,10 +18,7 @@ class SerializationException(SpeckleException):
self.unhandled_type = type(obj) self.unhandled_type = type(obj)
def __str__(self) -> str: def __str__(self) -> str:
return ( return f"SpeckleException: Could not serialize object of type {self.unhandled_type}"
"SpeckleException: Could not serialize object of type"
f" {self.unhandled_type}"
)
class GraphQLException(SpeckleException): class GraphQLException(SpeckleException):
@@ -1,14 +1,13 @@
import contextlib
import getpass
import hashlib
import logging
import platform
import queue
import sys import sys
import threading import queue
from typing import Optional import hashlib
import getpass
import logging
import requests import requests
import threading
import platform
import contextlib
""" """
Anonymous telemetry to help us understand how to make a better Speckle. Anonymous telemetry to help us understand how to make a better Speckle.
@@ -23,25 +22,22 @@ LOG = logging.getLogger(__name__)
METRICS_TRACKER = None METRICS_TRACKER = None
# actions # actions
SDK = "SDK Action"
CONNECTOR = "Connector Action"
RECEIVE = "Receive" RECEIVE = "Receive"
SEND = "Send" SEND = "Send"
# not in use since 2.15
ACCOUNTS = "Get Local Accounts"
BRANCH = "Branch Action"
CLIENT = "Speckle Client"
COMMIT = "Commit Action"
DESERIALIZE = "serialization/deserialize"
INVITE = "Invite Action"
OTHER_USER = "Other User Action"
PERMISSION = "Permission Action"
SERIALIZE = "serialization/serialize"
SERVER = "Server Action"
STREAM = "Stream Action" STREAM = "Stream Action"
STREAM_WRAPPER = "Stream Wrapper" PERMISSION = "Permission Action"
INVITE = "Invite Action"
COMMIT = "Commit Action"
BRANCH = "Branch Action"
USER = "User Action" USER = "User Action"
SERVER = "Server Action"
CLIENT = "Speckle Client"
STREAM_WRAPPER = "Stream Wrapper"
ACCOUNTS = "Get Local Accounts"
SERIALIZE = "serialization/serialize"
DESERIALIZE = "serialization/deserialize"
def disable(): def disable():
@@ -54,17 +50,13 @@ def enable():
TRACK = True TRACK = True
def set_host_app(host_app: str, host_app_version: Optional[str] = None): def set_host_app(host_app: str, host_app_version: str = None):
global HOST_APP, HOST_APP_VERSION global HOST_APP, HOST_APP_VERSION
HOST_APP = host_app HOST_APP = host_app
HOST_APP_VERSION = host_app_version or HOST_APP_VERSION HOST_APP_VERSION = host_app_version or HOST_APP_VERSION
def track( def track(action: str, account: "Account" = None, custom_props: dict = None):
action: str,
account=None,
custom_props: Optional[dict] = None,
):
if not TRACK: if not TRACK:
return return
try: try:
@@ -87,10 +79,10 @@ def track(
METRICS_TRACKER.queue.put_nowait(event_params) METRICS_TRACKER.queue.put_nowait(event_params)
except Exception as ex: 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)}") LOG.error(f"Error queueing metrics request: {str(ex)}")
def initialise_tracker(account=None): def initialise_tracker(account: "Account" = None):
global METRICS_TRACKER global METRICS_TRACKER
if not METRICS_TRACKER: if not METRICS_TRACKER:
METRICS_TRACKER = MetricsTracker() METRICS_TRACKER = MetricsTracker()
@@ -98,7 +90,7 @@ def initialise_tracker(account=None):
if account and account.userInfo.email: if account and account.userInfo.email:
METRICS_TRACKER.set_last_user(account.userInfo.email) METRICS_TRACKER.set_last_user(account.userInfo.email)
if account and account.serverInfo.url: if account and account.serverInfo.url:
METRICS_TRACKER.set_last_server(account.serverInfo.url) METRICS_TRACKER.set_last_server(account.userInfo.email)
class Singleton(type): class Singleton(type):
@@ -141,9 +133,7 @@ class MetricsTracker(metaclass=Singleton):
self.last_server = self.hash(server) self.last_server = self.hash(server)
def hash(self, value: str): def hash(self, value: str):
inputList = value.lower().split("://") return hashlib.md5(value.lower().encode("utf-8")).hexdigest().upper()
input = inputList[len(inputList) - 1].split("/")[0].split("?")[0]
return hashlib.md5(input.encode("utf-8")).hexdigest().upper()
def _send_tracking_requests(self): def _send_tracking_requests(self):
session = requests.Session() session = requests.Session()
@@ -153,6 +143,6 @@ class MetricsTracker(metaclass=Singleton):
try: try:
session.post(self.analytics_url, json=event_params) session.post(self.analytics_url, json=event_params)
except Exception as ex: except Exception as ex:
LOG.debug(f"Error sending metrics request: {str(ex)}") LOG.error(f"Error sending metrics request: {str(ex)}")
self.queue.task_done() self.queue.task_done()
+5
View File
@@ -0,0 +1,5 @@
"""Builtin Speckle object kit."""
from specklepy.objects.base import Base
__all__ = ["Base"]
@@ -1,25 +1,21 @@
import contextlib
from enum import Enum
from inspect import isclass
from typing import ( from typing import (
Any, Any,
ClassVar, ClassVar,
Dict, Dict,
ForwardRef,
List, List,
Optional, Optional,
Set,
Tuple,
Type,
Union, Union,
Set,
Type,
get_type_hints, get_type_hints,
) )
import contextlib
from enum import EnumMeta
from warnings import warn from warnings import warn
from stringcase import pascalcase from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.units import get_units_from_string
from specklepy.logging.exceptions import SpeckleException, SpeckleInvalidUnitException
from specklepy.objects.units import Units
from specklepy.transports.memory import MemoryTransport from specklepy.transports.memory import MemoryTransport
PRIMITIVES = (int, float, str, bool) PRIMITIVES = (int, float, str, bool)
@@ -94,9 +90,7 @@ class _RegisteringBase:
""" """
speckle_type: ClassVar[str] speckle_type: ClassVar[str]
_speckle_type_override: ClassVar[Optional[str]] = None _type_registry: ClassVar[Dict[str, "Base"]] = {}
_speckle_namespace: ClassVar[Optional[str]] = None
_type_registry: ClassVar[Dict[str, Type["Base"]]] = {}
_attr_types: ClassVar[Dict[str, Type]] = {} _attr_types: ClassVar[Dict[str, Type]] = {}
# dict of chunkable props and their max chunk size # dict of chunkable props and their max chunk size
_chunkable: Dict[str, int] = {} _chunkable: Dict[str, int] = {}
@@ -104,61 +98,22 @@ class _RegisteringBase:
_detachable: Set[str] = set() # list of defined detachable props _detachable: Set[str] = set() # list of defined detachable props
_serialize_ignore: Set[str] = set() _serialize_ignore: Set[str] = set()
class Config:
validate_assignment = True
@classmethod @classmethod
def get_registered_type(cls, speckle_type: str) -> Optional[Type["Base"]]: def get_registered_type(
cls, speckle_type: str
) -> Union["Base", Type["Base"], None]:
"""Get the registered type from the protected mapping via the `speckle_type`""" """Get the registered type from the protected mapping via the `speckle_type`"""
for full_name in reversed(speckle_type.split(":")): return cls._type_registry.get(speckle_type, None)
maybe_type = cls._type_registry.get(full_name, None)
if maybe_type:
return maybe_type
return None
@classmethod
def _determine_speckle_type(cls) -> str:
"""
This method brings the speckle_type construction in par with peckle-sharp/Core.
The implementation differs, because in Core the basis of the speckle_type if
type.FullName, which includes the dotnet namespace name too.
Copying that behavior is hard in python, where the concept of namespaces
means something entirely different.
So we enabled a speckle_type override mechanism, that enables
"""
base_name = "Base"
if cls.__name__ == base_name:
return base_name
bases = [
b._full_name()
for b in reversed(cls.mro())
if issubclass(b, Base) and b.__name__ != base_name
]
return ":".join(bases)
@classmethod
def _full_name(cls) -> str:
base_name = "Base"
if cls.__name__ == base_name:
return base_name
if cls._speckle_type_override:
return cls._speckle_type_override
# convert the module names to PascalCase to match c# namespace naming convention
# also drop specklepy from the beginning
namespace = ".".join(
pascalcase(m)
for m in filter(lambda name: name != "specklepy", cls.__module__.split("."))
)
return f"{namespace}.{cls.__name__}"
def __init_subclass__( def __init_subclass__(
cls, cls,
speckle_type: Optional[str] = None, speckle_type: str = None,
chunkable: Optional[Dict[str, int]] = None, chunkable: Dict[str, int] = None,
detachable: Optional[Set[str]] = None, detachable: Set[str] = None,
serialize_ignore: Optional[Set[str]] = None, serialize_ignore: Set[str] = None,
**kwargs: Dict[str, Any], **kwargs: Dict[str, Any],
): ):
""" """
@@ -168,15 +123,14 @@ class _RegisteringBase:
initialization. This is reused to register each subclassing type into a class initialization. This is reused to register each subclassing type into a class
level dictionary. level dictionary.
""" """
cls._speckle_type_override = speckle_type if speckle_type in cls._type_registry:
cls.speckle_type = cls._determine_speckle_type()
if cls._full_name() in cls._type_registry:
raise ValueError( raise ValueError(
f"The speckle_type: {speckle_type} is already registered for type: " f"The speckle_type: {speckle_type} is already registered for type: "
f"{cls._type_registry[cls._full_name()].__name__}. " f"{cls._type_registry[speckle_type].__name__}. "
"Please choose a different type name." f"Please choose a different type name."
) )
cls._type_registry[cls._full_name()] = cls # type: ignore cls.speckle_type = speckle_type or cls.__name__
cls._type_registry[cls.speckle_type] = cls # type: ignore
try: try:
cls._attr_types = get_type_hints(cls) cls._attr_types = get_type_hints(cls)
except Exception: except Exception:
@@ -188,142 +142,14 @@ class _RegisteringBase:
cls._detachable = cls._detachable.union(detachable) cls._detachable = cls._detachable.union(detachable)
if serialize_ignore: if serialize_ignore:
cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore) cls._serialize_ignore = cls._serialize_ignore.union(serialize_ignore)
# we know, that the super here is object, that takes no args on init subclass super().__init_subclass__(**kwargs)
return super().__init_subclass__()
# T = TypeVar("T")
# how i wish the code below would be correct, but we're also parsing into floats
# and converting into strings if the original type is string, but the value isn't
# def _validate_type(t: type, value: T) -> Tuple[bool, T]:
def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
# this should be reworked. Its only ok to return null for Optionals...
# if t is None and value is None:
if value is None:
return True, value
# after fixing the None t above, this should be
# if t is Any:
# if t is None:
if t is None or t is Any:
return True, value
if isclass(t) and issubclass(t, Enum):
if isinstance(value, t):
return True, value
if value in t._value2member_map_:
return True, t(value)
if getattr(t, "__module__", None) == "typing":
if isinstance(t, ForwardRef):
return True, value
origin = getattr(t, "__origin__")
# below is what in nicer for >= py38
# origin = get_origin(t)
# recursive validation for Unions on both types preferring the fist type
if origin is Union:
# below is what in nicer for >= py38
# t_1, t_2 = get_args(t)
args = t.__args__ # type: ignore
for arg_t in args:
t_success, t_value = _validate_type(arg_t, value)
if t_success:
return True, t_value
return False, value
if origin is dict:
if not isinstance(value, dict):
return False, value
if value == {}:
return True, value
if not getattr(t, "__args__", None):
return True, value
t_key, t_value = t.__args__ # type: ignore
if (
getattr(t_key, "__name__", None),
getattr(t_value, "__name__", None),
) == ("KT", "VT"):
return True, value
# we're only checking the first item, but the for loop and return after
# evaluating the first item is the fastest way
for dict_key, dict_value in value.items():
valid_key, _ = _validate_type(t_key, dict_key)
valid_value, _ = _validate_type(t_value, dict_value)
if valid_key and valid_value:
return True, value
return False, value
if origin is list:
if not isinstance(value, list):
return False, value
if value == []:
return True, value
if not hasattr(t, "__args__"):
return True, value
t_items = t.__args__[0] # type: ignore
if getattr(t_items, "__name__", None) == "T":
return True, value
first_item_valid, _ = _validate_type(t_items, value[0])
if first_item_valid:
return True, value
return False, value
if origin is tuple:
if not isinstance(value, tuple):
return False, value
if not hasattr(t, "__args__"):
return True, value
args = t.__args__ # type: ignore
if args == tuple():
return True, value
# we're not checking for empty tuple, cause tuple lengths must match
if len(args) != len(value):
return False, value
values = []
for t_item, v_item in zip(args, value):
item_valid, item_value = _validate_type(t_item, v_item)
if not item_valid:
return False, value
values.append(item_value)
return True, tuple(values)
if origin is set:
if not isinstance(value, set):
return False, value
if not hasattr(t, "__args__"):
return True, value
t_items = t.__args__[0] # type: ignore
first_item_valid, _ = _validate_type(t_items, next(iter(value)))
if first_item_valid:
return True, value
return False, value
if isinstance(value, t):
return True, value
with contextlib.suppress(ValueError, TypeError):
if t is float and value is not None:
return True, float(value)
# TODO: dafuq, i had to add this not list check
# but it would also fail for objects and other complex values
if t is str and value and not isinstance(value, list):
return True, str(value)
return False, value
class Base(_RegisteringBase): class Base(_RegisteringBase):
id: Union[str, None] = None id: Optional[str] = None
totalChildrenCount: Union[int, None] = None totalChildrenCount: Optional[int] = None
applicationId: Union[str, None] = None applicationId: Optional[str] = None
_units: Union[None, str] = None _units: Union[str, None] = None
def __init__(self, **kwargs) -> None: def __init__(self, **kwargs) -> None:
super().__init__() super().__init__()
@@ -391,14 +217,11 @@ class Base(_RegisteringBase):
@classmethod @classmethod
def update_forward_refs(cls) -> None: def update_forward_refs(cls) -> None:
""" """
Attempts to populate the internal defined types dict for type checking Attempts to populate the internal defined types dict for type checking sometime after defining the class.
sometime after defining the class. This is already done when defining the class, but can be called again if references to undefined types were
This is already done when defining the class, but can be called
again if references to undefined types were
included. included.
See `objects.geometry` for an example of how this is used with See `objects.geometry` for an example of how this is used with the Brep class definitions
the Brep class definitions.
""" """
try: try:
cls._attr_types = get_type_hints(cls) cls._attr_types = get_type_hints(cls)
@@ -419,27 +242,55 @@ class Base(_RegisteringBase):
"Invalid Name: Base member names cannot contain characters '.' or '/'", "Invalid Name: Base member names cannot contain characters '.' or '/'",
) )
def _type_check(self, name: str, value: Any) -> Any: def _type_check(self, name: str, value: Any):
""" """
Lightweight type checking of values before setting them Lightweight type checking of values before setting them
NOTE: Does not check subscripted types within generics as the performance hit NOTE: Does not check subscripted types within generics as the performance hit of checking
of checking each item within a given collection isn't worth it. each item within a given collection isn't worth it. Eg if you have a type Dict[str, float],
Eg if you have a type Dict[str, float],
we will only check if the value you're trying to set is a dict. we will only check if the value you're trying to set is a dict.
""" """
types = getattr(self, "_attr_types", {}) types = getattr(self, "_attr_types", {})
t = types.get(name, None) t = types.get(name, None)
valid, checked_value = _validate_type(t, value) if t is None or t is Any:
return value
if valid: if value is None:
return checked_value return None
if isinstance(t, EnumMeta) and (value in t._value2member_map_):
return t(value)
if t.__module__ == "typing":
origin = getattr(t, "__origin__")
t = (
tuple(getattr(sub_t, "__origin__", sub_t) for sub_t in t.__args__)
if origin is Union
else origin
)
if not isinstance(t, (type, tuple)):
warn(
f"Unrecognised type '{t}' provided for attribute '{name}'. Type will not been validated."
)
return value
if isinstance(value, t):
return value
# to be friendly, we'll parse ints and strs into floats, but not the other way around
# (to avoid unexpected rounding)
if isinstance(t, tuple):
t = t[0]
with contextlib.suppress(ValueError):
if t is float:
return float(value)
if t is str and value:
return str(value)
raise SpeckleException( raise SpeckleException(
f"Cannot set '{self.__class__.__name__}.{name}':" f"Cannot set '{self.__class__.__name__}.{name}': it expects type '{t.__name__}', but received type '{type(value).__name__}'"
f"it expects type '{str(t)}',"
f"but received type '{type(value).__name__}'"
) )
def add_chunkable_attrs(self, **kwargs: int) -> None: def add_chunkable_attrs(self, **kwargs: int) -> None:
@@ -447,8 +298,7 @@ class Base(_RegisteringBase):
Mark defined attributes as chunkable for serialisation Mark defined attributes as chunkable for serialisation
Arguments: Arguments:
kwargs {int} -- the name of the attribute as the keyword kwargs {int} -- the name of the attribute as the keyword and the chunk size as the arg
and the chunk size as the arg
""" """
chunkable = {k: v for k, v in kwargs.items() if isinstance(v, int)} chunkable = {k: v for k, v in kwargs.items() if isinstance(v, int)}
self._chunkable = dict(self._chunkable, **chunkable) self._chunkable = dict(self._chunkable, **chunkable)
@@ -458,25 +308,19 @@ class Base(_RegisteringBase):
Mark defined attributes as detachable for serialisation Mark defined attributes as detachable for serialisation
Arguments: Arguments:
names {Set[str]} -- the names of the attributes to detach as a set of string names {Set[str]} -- the names of the attributes to detach as a set of strings
""" """
self._detachable = self._detachable.union(names) self._detachable = self._detachable.union(names)
@property @property
def units(self) -> Union[str, None]: def units(self):
return self._units return self._units
@units.setter @units.setter
def units(self, value: Union[str, Units, None]): def units(self, value: str):
"""While this property accepts any string value, geometry expects units to be specific strings (see Units enum)""" units = get_units_from_string(value)
if isinstance(value, str) or value is None: if units:
self._units = value self._units = units
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]: def get_member_names(self) -> List[str]:
"""Get all of the property names on this object, dynamic or not""" """Get all of the property names on this object, dynamic or not"""
@@ -506,17 +350,13 @@ class Base(_RegisteringBase):
def get_id(self, decompose: bool = False) -> str: def get_id(self, decompose: bool = False) -> str:
""" """
Gets the id (a unique hash) of this object. Gets the id (a unique hash) of this object. This method fully serializes the object which,
This method fully serializes the object which, in the case of large objects (with many sub-objects), has a tangible cost. Avoid using it!
in the case of large objects (with many sub-objects), has a tangible cost.
Avoid using it!
Note: the hash of a decomposed object differs from that of a Note: the hash of a decomposed object differs from that of a non-decomposed object
non-decomposed object
Arguments: Arguments:
decompose {bool} -- if True, will decompose the object in decompose {bool} -- if True, will decompose the object in the process of hashing it
the process of hashing it
Returns: Returns:
str -- the hash (id) of the fully serialized object str -- the hash (id) of the fully serialized object
@@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Type from typing import Any, Callable, List, Type, Dict
from specklepy.logging.exceptions import SpeckleException from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base from specklepy.objects.base import Base
@@ -43,7 +43,7 @@ def curve_from_list(args: List[float]):
class ObjectArray: class ObjectArray:
def __init__(self, data: Optional[list] = None) -> None: def __init__(self, data: list = None) -> None:
self.data = data or [] self.data = data or []
@classmethod @classmethod
@@ -68,7 +68,7 @@ class ObjectArray:
def decode_data( def decode_data(
data: List[Any], decoder: Callable[[List[Any]], Base], **kwargs: Dict[str, Any] data: List[Any], decoder: Callable[[List[Any]], Base], **kwargs: Dict[str, Any]
) -> List[Base]: ) -> List[Base]:
bases: List[Base] = [] bases = []
if not data: if not data:
return bases return bases
index = 0 index = 0
@@ -1,6 +1,5 @@
from enum import Enum from enum import Enum
from typing import List, Optional from typing import List
from specklepy.objects.geometry import Point from specklepy.objects.geometry import Point
from .base import Base from .base import Base
@@ -17,8 +16,8 @@ DETACHABLE = {"detach_this", "origin", "detached_list"}
class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}): class FakeGeo(Base, chunkable={"dots": 50}, detachable={"pointslist"}):
pointslist: Optional[List[Base]] = None pointslist: List[Base] = None
dots: Optional[List[int]] = None dots: List[int] = None
class FakeDirection(Enum): class FakeDirection(Enum):
@@ -29,15 +28,15 @@ class FakeDirection(Enum):
class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE): class FakeMesh(FakeGeo, chunkable=CHUNKABLE_PROPS, detachable=DETACHABLE):
vertices: Optional[List[float]] = None vertices: List[float] = None
faces: Optional[List[int]] = None faces: List[int] = None
colors: Optional[List[int]] = None colors: List[int] = None
textureCoordinates: Optional[List[float]] = None textureCoordinates: List[float] = None
cardinal_dir: Optional[FakeDirection] = None cardinal_dir: FakeDirection = None
test_bases: Optional[List[Base]] = None test_bases: List[Base] = None
detach_this: Optional[Base] = None detach_this: Base = None
detached_list: Optional[List[Base]] = None detached_list: List[Base] = None
_origin: Optional[Point] = None _origin: Point = None
# def __init__(self, **kwargs) -> None: # def __init__(self, **kwargs) -> None:
# super(FakeMesh, self).__init__(**kwargs) # super(FakeMesh, self).__init__(**kwargs)
@@ -1,31 +1,39 @@
from enum import Enum from enum import Enum
from typing import Any, List, Optional from typing import Any, List, Optional
from specklepy.objects.base import Base from .base import Base
from specklepy.objects.encoding import CurveArray, CurveTypeEncoding, ObjectArray from .encoding import CurveArray, CurveTypeEncoding, ObjectArray
from specklepy.objects.primitive import Interval from .units import get_encoding_from_units, get_units_from_encoding
from specklepy.objects.units import get_encoding_from_units, get_units_from_encoding
GEOMETRY = "Objects.Geometry." GEOMETRY = "Objects.Geometry."
class Interval(Base, speckle_type="Objects.Primitive.Interval"):
start: float = 0.0
end: float = 0.0
def length(self):
return abs(self.start - self.end)
@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 Point(Base, speckle_type=GEOMETRY + "Point"): class Point(Base, speckle_type=GEOMETRY + "Point"):
x: float = 0.0 x: float = 0.0
y: float = 0.0 y: float = 0.0
z: float = 0.0 z: float = 0.0
def __repr__(self) -> str: def __repr__(self) -> str:
return ( return f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id: {self.id}, speckle_type: {self.speckle_type})"
f"{self.__class__.__name__}(x: {self.x}, y: {self.y}, z: {self.z}, id:"
f" {self.id}, speckle_type: {self.speckle_type})"
)
@classmethod @classmethod
def from_list(cls, args: List[float]) -> "Point": 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"""
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]) return cls(x=args[0], y=args[1], z=args[2])
def to_list(self) -> List[Any]: def to_list(self) -> List[Any]:
@@ -39,50 +47,12 @@ class Point(Base, speckle_type=GEOMETRY + "Point"):
return pt return pt
class Pointcloud( class Vector(Point, speckle_type=GEOMETRY + "Vector"):
Base, pass
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"): class ControlPoint(Point, speckle_type=GEOMETRY + "ControlPoint"):
weight: Optional[float] = None weight: float = None
class Plane(Base, speckle_type=GEOMETRY + "Plane"): class Plane(Base, speckle_type=GEOMETRY + "Plane"):
@@ -107,25 +77,25 @@ class Plane(Base, speckle_type=GEOMETRY + "Plane"):
*self.normal.to_list(), *self.normal.to_list(),
*self.xdir.to_list(), *self.xdir.to_list(),
*self.ydir.to_list(), *self.ydir.to_list(),
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
class Box(Base, speckle_type=GEOMETRY + "Box"): class Box(Base, speckle_type=GEOMETRY + "Box"):
basePlane: Plane = Plane() basePlane: Plane = Plane()
xSize: Interval = Interval()
ySize: Interval = Interval() ySize: Interval = Interval()
zSize: Interval = Interval() zSize: Interval = Interval()
area: Optional[float] = None xSize: Interval = Interval()
volume: Optional[float] = None area: float = None
volume: float = None
class Line(Base, speckle_type=GEOMETRY + "Line"): class Line(Base, speckle_type=GEOMETRY + "Line"):
start: Point = Point() start: Point = Point()
end: Optional[Point] = None end: Point = None
domain: Optional[Interval] = None domain: Interval = None
bbox: Optional[Box] = None bbox: Box = None
length: Optional[float] = None length: float = None
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Line": def from_list(cls, args: List[Any]) -> "Line":
@@ -143,23 +113,23 @@ class Line(Base, speckle_type=GEOMETRY + "Line"):
*self.start.to_list(), *self.start.to_list(),
*self.end.to_list(), *self.end.to_list(),
*domain, *domain,
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
class Arc(Base, speckle_type=GEOMETRY + "Arc"): class Arc(Base, speckle_type=GEOMETRY + "Arc"):
radius: Optional[float] = None radius: float = None
startAngle: Optional[float] = None startAngle: float = None
endAngle: Optional[float] = None endAngle: float = None
angleRadians: Optional[float] = None angleRadians: float = None
plane: Optional[Plane] = None plane: Plane = None
domain: Optional[Interval] = None domain: Interval = None
startPoint: Optional[Point] = None startPoint: Point = None
midPoint: Optional[Point] = None midPoint: Point = None
endPoint: Optional[Point] = None endPoint: Point = None
bbox: Optional[Box] = None bbox: Box = None
area: Optional[float] = None area: float = None
length: Optional[float] = None length: float = None
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Arc": def from_list(cls, args: List[Any]) -> "Arc":
@@ -188,17 +158,17 @@ class Arc(Base, speckle_type=GEOMETRY + "Arc"):
*self.startPoint.to_list(), *self.startPoint.to_list(),
*self.midPoint.to_list(), *self.midPoint.to_list(),
*self.endPoint.to_list(), *self.endPoint.to_list(),
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
class Circle(Base, speckle_type=GEOMETRY + "Circle"): class Circle(Base, speckle_type=GEOMETRY + "Circle"):
radius: Optional[float] = None radius: float = None
plane: Optional[Plane] = None plane: Plane = None
domain: Optional[Interval] = None domain: Interval = None
bbox: Optional[Box] = None bbox: Box = None
area: Optional[float] = None area: float = None
length: Optional[float] = None length: float = None
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Circle": def from_list(cls, args: List[Any]) -> "Circle":
@@ -215,19 +185,19 @@ class Circle(Base, speckle_type=GEOMETRY + "Circle"):
self.radius, self.radius,
*self.domain.to_list(), *self.domain.to_list(),
*self.plane.to_list(), *self.plane.to_list(),
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"): class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
firstRadius: Optional[float] = None firstRadius: float = None
secondRadius: Optional[float] = None secondRadius: float = None
plane: Optional[Plane] = None plane: Plane = None
domain: Optional[Interval] = None domain: Interval = None
trimDomain: Optional[Interval] = None trimDomain: Interval = None
bbox: Optional[Box] = None bbox: Box = None
area: Optional[float] = None area: float = None
length: Optional[float] = None length: float = None
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Ellipse": def from_list(cls, args: List[Any]) -> "Ellipse":
@@ -246,17 +216,17 @@ class Ellipse(Base, speckle_type=GEOMETRY + "Ellipse"):
self.secondRadius, self.secondRadius,
*self.domain.to_list(), *self.domain.to_list(),
*self.plane.to_list(), *self.plane.to_list(),
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}): class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 20000}):
value: Optional[List[float]] = None value: List[float] = None
closed: Optional[bool] = None closed: bool = None
domain: Optional[Interval] = None domain: Interval = None
bbox: Optional[Box] = None bbox: Box = None
area: Optional[float] = None area: float = None
length: Optional[float] = None length: float = None
@classmethod @classmethod
def from_points(cls, points: List[Point]): def from_points(cls, points: List[Point]):
@@ -285,7 +255,7 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
*self.domain.to_list(), *self.domain.to_list(),
len(self.value), len(self.value),
*self.value, *self.value,
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
def as_points(self) -> List[Point]: def as_points(self) -> List[Point]:
@@ -302,50 +272,23 @@ class Polyline(Base, speckle_type=GEOMETRY + "Polyline", chunkable={"value": 200
] ]
class SpiralType(Enum):
Biquadratic = 0
BiquadraticParabola = 1
Bloss = 2
Clothoid = 3
Cosine = 4
Cubic = 5
CubicParabola = 6
Radioid = 7
Sinusoid = 8
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( class Curve(
Base, Base,
speckle_type=GEOMETRY + "Curve", speckle_type=GEOMETRY + "Curve",
chunkable={"points": 20000, "weights": 20000, "knots": 20000}, chunkable={"points": 20000, "weights": 20000, "knots": 20000},
): ):
degree: Optional[int] = None degree: int = None
periodic: Optional[bool] = None periodic: bool = None
rational: Optional[bool] = None rational: bool = None
points: Optional[List[float]] = None points: List[float] = None
weights: Optional[List[float]] = None weights: List[float] = None
knots: Optional[List[float]] = None knots: List[float] = None
domain: Optional[Interval] = None domain: Interval = None
displayValue: Optional[Polyline] = None displayValue: Polyline = None
closed: Optional[bool] = None closed: bool = None
bbox: Optional[Box] = None bbox: Box = None
area: Optional[float] = None area: float = None
length: Optional[float] = None length: float = None
def as_points(self) -> List[Point]: def as_points(self) -> List[Point]:
"""Converts the `value` attribute to a list of Points""" """Converts the `value` attribute to a list of Points"""
@@ -397,17 +340,17 @@ class Curve(
*self.points, *self.points,
*self.weights, *self.weights,
*self.knots, *self.knots,
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"): class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
segments: Optional[List[Base]] = None segments: List[Base] = None
domain: Optional[Interval] = None domain: Interval = None
closed: Optional[bool] = None closed: bool = None
bbox: Optional[Box] = None bbox: Box = None
area: Optional[float] = None area: float = None
length: Optional[float] = None length: float = None
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Polycurve": def from_list(cls, args: List[Any]) -> "Polycurve":
@@ -427,22 +370,22 @@ class Polycurve(Base, speckle_type=GEOMETRY + "Polycurve"):
*self.domain.to_list(), *self.domain.to_list(),
len(curve_array), len(curve_array),
*curve_array, *curve_array,
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"): class Extrusion(Base, speckle_type=GEOMETRY + "Extrusion"):
capped: Optional[bool] = None capped: bool = None
profile: Optional[Base] = None profile: Base = None
pathStart: Optional[Point] = None pathStart: Point = None
pathEnd: Optional[Point] = None pathEnd: Point = None
pathCurve: Optional[Base] = None pathCurve: Base = None
pathTangent: Optional[Base] = None pathTangent: Base = None
profiles: Optional[List[Base]] = None profiles: List[Base] = None
length: Optional[float] = None length: float = None
area: Optional[float] = None area: float = None
volume: Optional[float] = None volume: float = None
bbox: Optional[Box] = None bbox: Box = None
class Mesh( class Mesh(
@@ -455,21 +398,21 @@ class Mesh(
"textureCoordinates": 2000, "textureCoordinates": 2000,
}, },
): ):
vertices: Optional[List[float]] = None vertices: List[float] = None
faces: Optional[List[int]] = None faces: List[int] = None
colors: Optional[List[int]] = None colors: List[int] = None
textureCoordinates: Optional[List[float]] = None textureCoordinates: List[float] = None
bbox: Optional[Box] = None bbox: Box = None
area: Optional[float] = None area: float = None
volume: Optional[float] = None volume: float = None
@classmethod @classmethod
def create( def create(
cls, cls,
vertices: List[float], vertices: List[float],
faces: List[int], faces: List[int],
colors: Optional[List[int]] = None, colors: List[int] = None,
texture_coordinates: Optional[List[float]] = None, texture_coordinates: List[float] = None,
) -> "Mesh": ) -> "Mesh":
""" """
Create a new Mesh from lists representing its vertices, faces, Create a new Mesh from lists representing its vertices, faces,
@@ -487,20 +430,20 @@ class Mesh(
class Surface(Base, speckle_type=GEOMETRY + "Surface"): class Surface(Base, speckle_type=GEOMETRY + "Surface"):
degreeU: Optional[int] = None degreeU: int = None
degreeV: Optional[int] = None degreeV: int = None
rational: Optional[bool] = None rational: bool = None
area: Optional[float] = None area: float = None
pointData: Optional[List[float]] = None pointData: List[float] = None
countU: Optional[int] = None countU: int = None
countV: Optional[int] = None countV: int = None
bbox: Optional[Box] = None bbox: Box = None
closedU: Optional[bool] = None closedU: bool = None
closedV: Optional[bool] = None closedV: bool = None
domainU: Optional[Interval] = None domainU: Interval = None
domainV: Optional[Interval] = None domainV: Interval = None
knotsU: Optional[List[float]] = None knotsU: List[float] = None
knotsV: Optional[List[float]] = None knotsV: List[float] = None
@classmethod @classmethod
def from_list(cls, args: List[Any]) -> "Surface": def from_list(cls, args: List[Any]) -> "Surface":
@@ -545,16 +488,16 @@ class Surface(Base, speckle_type=GEOMETRY + "Surface"):
*self.pointData, *self.pointData,
*self.knotsU, *self.knotsU,
*self.knotsV, *self.knotsV,
get_encoding_from_units(self._units), get_encoding_from_units(self.units),
] ]
class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"): class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
_Brep: Optional["Brep"] = None _Brep: "Brep" = None
SurfaceIndex: Optional[int] = None SurfaceIndex: int = None
OuterLoopIndex: Optional[int] = None OuterLoopIndex: int = None
OrientationReversed: Optional[bool] = None OrientationReversed: bool = None
LoopIndices: Optional[List[int]] = None LoopIndices: List[int] = None
@property @property
def _outer_loop(self): def _outer_loop(self):
@@ -590,13 +533,13 @@ class BrepFace(Base, speckle_type=GEOMETRY + "BrepFace"):
class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"): class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
_Brep: Optional["Brep"] = None _Brep: "Brep" = None
Curve3dIndex: Optional[int] = None Curve3dIndex: int = None
TrimIndices: Optional[List[int]] = None TrimIndices: List[int] = None
StartIndex: Optional[int] = None StartIndex: int = None
EndIndex: Optional[int] = None EndIndex: int = None
ProxyCurveIsReversed: Optional[bool] = None ProxyCurveIsReversed: bool = None
Domain: Optional[Interval] = None Domain: Interval = None
@property @property
def _start_vertex(self): def _start_vertex(self):
@@ -647,6 +590,7 @@ class BrepEdge(Base, speckle_type=GEOMETRY + "BrepEdge"):
] ]
class BrepLoopType(int, Enum): class BrepLoopType(int, Enum):
Unknown = 0 Unknown = 0
Outer = 1 Outer = 1
@@ -657,10 +601,10 @@ class BrepLoopType(int, Enum):
class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"): class BrepLoop(Base, speckle_type=GEOMETRY + "BrepLoop"):
_Brep: Optional["Brep"] = None _Brep: "Brep" = None
FaceIndex: Optional[Optional[int]] = None FaceIndex: int = None
TrimIndices: Optional[List[int]] = None TrimIndices: List[int] = None
Type: Optional[BrepLoopType] = None Type: BrepLoopType = None
@property @property
def _face(self): def _face(self):
@@ -701,17 +645,17 @@ class BrepTrimType(int, Enum):
class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"): class BrepTrim(Base, speckle_type=GEOMETRY + "BrepTrim"):
_Brep: Optional["Brep"] = None _Brep: "Brep" = None
EdgeIndex: Optional[int] = None EdgeIndex: int = None
StartIndex: Optional[int] = None StartIndex: int = None
EndIndex: Optional[int] = None EndIndex: int = None
FaceIndex: Optional[int] = None FaceIndex: int = None
LoopIndex: Optional[int] = None LoopIndex: int = None
CurveIndex: Optional[int] = None CurveIndex: int = None
IsoStatus: Optional[int] = None IsoStatus: int = None
TrimType: Optional[BrepTrimType] = None TrimType: BrepTrimType = None
IsReversed: Optional[bool] = None IsReversed: bool = None
Domain: Optional[Interval] = None Domain: Interval = None
@property @property
def _face(self): def _face(self):
@@ -788,21 +732,21 @@ class Brep(
"Faces", "Faces",
}, },
): ):
provenance: Optional[str] = None provenance: str = None
bbox: Optional[Box] = None bbox: Box = None
area: Optional[float] = None area: float = None
volume: Optional[float] = None volume: float = None
_displayValue: Optional[List[Mesh]] = None _displayValue: List[Mesh] = None
Surfaces: Optional[List[Surface]] = None Surfaces: List[Surface] = None
Curve3D: Optional[List[Base]] = None Curve3D: List[Base] = None
Curve2D: Optional[List[Base]] = None Curve2D: List[Base] = None
Vertices: Optional[List[Point]] = None Vertices: List[Point] = None
Edges: Optional[List[BrepEdge]] = None Edges: List[BrepEdge] = None
Loops: Optional[List[BrepLoop]] = None Loops: List[BrepLoop] = None
Faces: Optional[List[BrepFace]] = None Faces: List[BrepFace] = None
Trims: Optional[List[BrepTrim]] = None Trims: List[BrepTrim] = None
IsClosed: Optional[bool] = None IsClosed: bool = None
Orientation: Optional[int] = None Orientation: int = None
def _inject_self_into_children(self, children: Optional[List[Base]]) -> List[Base]: def _inject_self_into_children(self, children: Optional[List[Base]]) -> List[Base]:
if children is None: if children is None:
@@ -913,7 +857,7 @@ class Brep(
for i in range(0, len(value), 3): for i in range(0, len(value), 3):
vertex = Point.from_list(value[i : i + 3]) vertex = Point.from_list(value[i : i + 3])
vertex.units = units vertex._units = units
vertices.append(vertex) vertices.append(vertex)
self.Vertices = vertices self.Vertices = vertices
@@ -1,9 +1,5 @@
from typing import List, Optional from typing import Any, List
from deprecated import deprecated
from specklepy.objects.geometry import Point, Vector from specklepy.objects.geometry import Point, Vector
from .base import Base from .base import Base
OTHER = "Objects.Other." OTHER = "Objects.Other."
@@ -29,7 +25,7 @@ IDENTITY_TRANSFORM = [
class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"): class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
name: Optional[str] = None name: str = None
opacity: float = 1 opacity: float = 1
metalness: float = 0 metalness: float = 0
roughness: float = 1 roughness: float = 1
@@ -37,60 +33,36 @@ class RenderMaterial(Base, speckle_type=OTHER + "RenderMaterial"):
emissive: int = -16777216 # black arbg emissive: int = -16777216 # black arbg
class RenderMaterialProxy(
Base,
speckle_type="Speckle.Core.Models.Proxies.RenderMaterialProxy",
):
"""
Used to store render material to object relationships in root collections.
"""
objects: list[str]
value: RenderMaterial
class Transform( class Transform(
Base, Base,
speckle_type=OTHER + "Transform", speckle_type=OTHER + "Transform",
serialize_ignore={"translation", "scaling", "is_identity", "value"}, serialize_ignore={"translation", "scaling", "is_identity"},
): ):
"""The 4x4 transformation matrix """The 4x4 transformation matrix
The 3x3 sub-matrix determines scaling. The 3x3 sub-matrix determines scaling.
The 4th column defines translation, The 4th column defines translation, where the last value is a divisor (usually equal to 1).
where the last value is a divisor (usually equal to 1).
""" """
_value: Optional[List[float]] = None _value: List[float] = None
@property @property
@deprecated(version="2.12", reason="Use matrix")
def value(self) -> List[float]: def value(self) -> List[float]:
"""The transform matrix represented as a flat list of 16 floats"""
return self._value return self._value
@value.setter @value.setter
def value(self, value: List[float]) -> None: 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: try:
value = [float(x) for x in value] value = [float(x) for x in value]
except (ValueError, TypeError) as error: except (ValueError, TypeError) as error:
raise ValueError( raise ValueError(
"Could not create a Transform object with the requested value. Input" f"Could not create a Transform object with the requested value. Input must be a 16 element list of numbers. Value provided: {value}"
f" must be a 16 element list of numbers. Value provided: {value}"
) from error ) from error
if len(value) != 16: if len(value) != 16:
raise ValueError( raise ValueError(
"Could not create a Transform object: input list should be 16 floats" f"Could not create a Transform object: input list should be 16 floats long, but was {len(value)} long"
f" long, but was {len(value)} long"
) )
self._value = value self._value = value
@@ -107,7 +79,7 @@ class Transform(
@property @property
def is_identity(self) -> bool: def is_identity(self) -> bool:
return self._value == IDENTITY_TRANSFORM return self.value == IDENTITY_TRANSFORM
def apply_to_point(self, point: Point) -> Point: def apply_to_point(self, point: Point) -> Point:
"""Transform a single speckle Point """Transform a single speckle Point
@@ -155,16 +127,14 @@ class Transform(
"""Transform a list of speckle Points """Transform a list of speckle Points
Arguments: Arguments:
points {List[float]} points {List[float]} -- a flat list of floats representing points to transform
-- a flat list of floats representing points to transform
Returns: Returns:
List[float] -- a new transformed list List[float] -- a new transformed list
""" """
if len(points_value) % 3 != 0: if len(points_value) % 3 != 0:
raise ValueError( raise ValueError(
"Cannot apply transform as the points list is malformed: expected" "Cannot apply transform as the points list is malformed: expected length to be multiple of 3"
" length to be multiple of 3"
) )
transformed = [] transformed = []
for i in range(0, len(points_value), 3): for i in range(0, len(points_value), 3):
@@ -201,13 +171,11 @@ class Transform(
][:3] ][:3]
@classmethod @classmethod
def from_list(cls, value: Optional[List[float]] = None) -> "Transform": def from_list(cls, value: List[float] = None) -> "Transform":
"""Returns a Transform object from a list of 16 numbers. """Returns a Transform object from a list of 16 numbers. If no value is provided, an identity transform will be returned.
If no value is provided, an identity transform will be returned.
Arguments: Arguments:
value {List[float]} -- the matrix as a flat list of 16 numbers value {List[float]} -- the matrix as a flat list of 16 numbers (defaults to the identity transform)
(defaults to the identity transform)
Returns: Returns:
Transform -- a complete transform object Transform -- a complete transform object
@@ -217,10 +185,30 @@ class Transform(
return cls(value=value) return cls(value=value)
class Collection( class BlockDefinition(
Base, Base, speckle_type=OTHER + "BlockDefinition", detachable={"geometry"}
speckle_type="Speckle.Core.Models.Collections.Collection",
detachable={"elements"},
): ):
name: Optional[str] = None name: str = None
elements: Optional[List[Base]] = None basePoint: Point = None
geometry: List[Base] = None
class BlockInstance(
Base, speckle_type=OTHER + "BlockInstance", detachable={"blockDefinition"}
):
blockDefinition: BlockDefinition = None
transform: Transform = None
# 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: str = None
value: Any = None
applicationUnitType: str = None # eg UnitType UT_Length
applicationUnit: str = None # DisplayUnitType eg DUT_MILLIMITERS
applicationInternalName: str = (
None # BuiltInParameterName or GUID for shared parameter
)
isShared: bool = False
isReadOnly: bool = False
isTypeParameter: bool = False
+40
View File
@@ -0,0 +1,40 @@
"""Builtin Speckle object kit."""
from specklepy.objects.structural.analysis import *
from specklepy.objects.structural.properties import *
from specklepy.objects.structural.material import *
from specklepy.objects.structural.geometry import *
from specklepy.objects.structural.loading import *
from specklepy.objects.structural.axis import Axis
__all__ = [
"Element1D",
"Element2D",
"Element3D",
"Axis",
"Node",
"Restraint",
"Load",
"LoadBeam",
"LoadCase",
"LoadCombinations",
"LoadFace",
"LoadGravity",
"LoadNode",
"Model",
"ModelInfo",
"ModelSettings",
"ModelUnits",
"Concrete",
"Material",
"Steel",
"Timber",
"Property",
"Property1D",
"Property2D",
"Property3D",
"PropertyDamper",
"PropertyMass",
"PropertySpring",
"SectionProfile",
]
+51
View File
@@ -0,0 +1,51 @@
from typing import List
from ..base import Base
from ..geometry import *
from .properties import *
STRUCTURAL_ANALYSIS = "Objects.Structural.Analysis."
class ModelUnits(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelUnits"):
length: str = None
sections: str = None
displacements: str = None
stress: str = None
force: str = None
mass: str = None
time: str = None
temperature: str = None
velocity: str = None
acceleration: str = None
energy: str = None
angle: str = None
strain: str = None
class ModelSettings(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelSettings"):
modelUnits: ModelUnits = None
steelCode: str = None
concreteCode: str = None
coincidenceTolerance: float = 0.0
class ModelInfo(Base, speckle_type=STRUCTURAL_ANALYSIS + "ModelInfo"):
name: str = None
description: str = None
projectNumber: str = None
projectName: str = None
settings: ModelSettings = None
initials: str = None
application: str = None
class Model(Base, speckle_type=STRUCTURAL_ANALYSIS + "Model"):
specs: ModelInfo = None
nodes: List = None
elements: List = None
loads: List = None
restraints: List = None
properties: List = None
materials: List = None
layerDescription: str = None
+8
View File
@@ -0,0 +1,8 @@
from ..base import Base
from ..geometry import Plane
class Axis(Base, speckle_type="Objects.Structural.Geometry.Axis"):
name: str = None
axisType: str = None
plane: Plane = None
+108
View File
@@ -0,0 +1,108 @@
from enum import Enum
from typing import List
from ..base import Base
from ..geometry import *
from .properties import *
from .axis import Axis
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: 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
units: str = None
class Node(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Node"):
name: str = None
basePoint: Point = None
constraintAxis: Axis = None
restraint: Restraint = None
springProperty: PropertySpring = None
massProperty: PropertyMass = None
damperProperty: PropertyDamper = None
units: str = None
class Element1D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element1D"):
name: str = None
baseLine: Line = None
property: Property1D = None
type: ElementType1D = None
end1Releases: Restraint = None
end2Releases: Restraint = None
end1Offset: Vector = None
end2Offset: Vector = None
orientationNode: Node = None
orinetationAngle: float = 0.0
localAxis: Plane = None
parent: Base = None
end1Node: Node = Node
end2Node: Node = Node
topology: List = None
displayMesh: Mesh = None
units: str = None
class Element2D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element2D"):
name: str = None
property: Property2D = None
type: ElementType2D = None
offset: float = 0.0
orientationAngle: float = 0.0
parent: Base = None
topology: List = None
displayMesh: Mesh = None
units: str = None
class Element3D(Base, speckle_type=STRUCTURAL_GEOMETRY + ".Element3D"):
name: str = None
baseMesh: Mesh = None
property: Property3D = None
type: ElementType3D = None
orientationAngle: float = 0.0
parent: Base = None
topology: List
units: str = None
# class Storey needs ependency on built elements first
+144
View File
@@ -0,0 +1,144 @@
from enum import Enum
from typing import List
from ..base import Base
from .geometry import *
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: str = None
loadType: LoadType = None
group: str = None
actionType: ActionType = None
description: str = None
class Load(Base, speckle_type=STRUCTURAL_LOADING + "Load"):
name: str = None
units: str = None
loadCase: LoadCase = None
class LoadBeam(Load, speckle_type=STRUCTURAL_LOADING + "LoadBeam"):
elements: List = None
loadType: BeamLoadType = None
direction: LoadDirection = None
loadAxis: Axis = None
loadAxisType: LoadAxisType = None
isProjected: bool = None
values: List = None
positions: List = None
class LoadCombinations(Base, speckle_type=STRUCTURAL_LOADING + "LoadCombination"):
name: str = None
loadCases: List
loadFactors: List
combinationType: CombinationType
class LoadFace(Load, speckle_type=STRUCTURAL_LOADING + "LoadFace"):
elements: List = None
loadType: FaceLoadType = None
direction: LoadDirection2D = None
loadAxis: Axis = None
loadAxisType: LoadAxisType = None
isProjected: bool = None
values: List = None
positions: List = None
class LoadGravity(Load, speckle_type=STRUCTURAL_LOADING + "LoadGravity"):
elements: List = None
nodes: List = None
gravityFactors: Vector = None
class LoadNode(Load, speckle_type=STRUCTURAL_LOADING + "LoadNode"):
nodes: List = None
loadAxis: Axis = None
direction: LoadDirection = None
value: float = 0.0
+59
View File
@@ -0,0 +1,59 @@
from enum import Enum
from ..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 Material(Base, speckle_type=STRUCTURAL_MATERIALS):
name: str = None
grade: str = None
materialType: MaterialType = None
designCode: str = None
codeYear: 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(Material, speckle_type=STRUCTURAL_MATERIALS + ".Concrete"):
compressiveStrength: float = 0.0
tensileStrength: float = 0.0
flexuralStrength: float = 0.0
maxCompressiveStrength: float = 0.0
maxTensileStrength: float = 0.0
maxAggregateSize: float = 0.0
lightweight: bool = None
class Steel(Material, speckle_type=STRUCTURAL_MATERIALS + ".Steel"):
yieldStrength: float = 0.0
ultimateStrength: float = 0.0
maxStrain: float = 0.0
strainHardeningModulus: float = 0.0
class Timber(Material, speckle_type=STRUCTURAL_MATERIALS + ".Timber"):
species: str = None
+212
View File
@@ -0,0 +1,212 @@
from enum import Enum
from ..base import Base
from .material import *
from .axis import Axis
STRUCTURAL_PROPERTY = "Objectives.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
Tee = 3
Angle = 4
Channel = 5
Perimeter = 6
Box = 7
Catalogue = 8
Explicit = 9
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: str = None
class SectionProfile(Base, speckle_type=STRUCTURAL_PROPERTY + ".SectionProfile"):
name: str = None
shapeType: 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
units: str = None
class Property1D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property1D"):
memberType: MemberType = None
Material: Material = None
SectionProfile: SectionProfile = None
BaseReferencePoint: BaseReferencePoint = None
offsetY: float = 0.0
offsetZ: float = 0.0
class Property2D(Property, speckle_type=STRUCTURAL_PROPERTY + ".Property2D"):
PropertyType2D: PropertyType2D = None
thickness: float = 0.0
Material: Material = None
axis: Axis = None
referenceSurface: 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"):
PropertyType3D: PropertyType3D = None
Material: Material = None
axis: Axis = None
class PropertyDamper(Property, speckle_type=STRUCTURAL_PROPERTY + ".PropertyDamper"):
damperType: 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: bool = None
massModifierX: float = 0.0
massModifierY: float = 0.0
massModifierZ: float = 0.0
class PropertySpring(Property, speckle_type=STRUCTURAL_PROPERTY + ".PropertySpring"):
springType: 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
+174
View File
@@ -0,0 +1,174 @@
from typing import List
from ..base import Base
from ..geometry import *
from .loading import *
from .geometry import *
from .analysis import Model
STRUCTURAL_RESULTS = "Objects.Structural.Results."
class Result(Base, speckle_type=STRUCTURAL_RESULTS + "Result"):
resultCase: Base = None
permutation: str = None
description: str = None
class ResultSet1D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet1D"):
results1D: List
class Result1D(Result, speckle_type=STRUCTURAL_RESULTS + "Result1D"):
element: Element1D = None
position: float = 0.0
dispX: float = 0.0
dispY: float = 0.0
dispZ: float = 0.0
rotXX: float = 0.0
rotYY: float = 0.0
rotZZ: float = 0.0
forceX: float = 0.0
forceY: float = 0.0
forceZ: float = 0.0
momentXX: float = 0.0
momentYY: float = 0.0
momentZZ: float = 0.0
axialStress: float = 0.0
shearStressY: float = 0.0
shearStressZ: float = 0.0
bendingStressYPos: float = 0.0
bendingStressYNeg: float = 0.0
bendingStressZPos: float = 0.0
bendingStressZNeg: float = 0.0
combinedStressMax: float = 0.0
combinedStressMin: float = 0.0
class ResultSet2D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet2D"):
results2D: List
class Result2D(Result, speckle_type=STRUCTURAL_RESULTS + "Result2D"):
element: Element2D = None
position: List
dispX: float = 0.0
dispY: float = 0.0
dispZ: float = 0.0
forceXX: float = 0.0
forceYY: float = 0.0
forceXY: float = 0.0
momentXX: float = 0.0
momentYY: float = 0.0
momentXY: float = 0.0
shearX: float = 0.0
shearY: float = 0.0
stressTopXX: float = 0.0
stressTopYY: float = 0.0
stressTopZZ: float = 0.0
stressTopXY: float = 0.0
stressTopYZ: float = 0.0
stressTopZX: float = 0.0
stressMidXX: float = 0.0
stressMidYY: float = 0.0
stressMidZZ: float = 0.0
stressMidXY: float = 0.0
stressMidYZ: float = 0.0
stressMidZX: float = 0.0
stressBotXX: float = 0.0
stressBotYY: float = 0.0
stressBotZZ: float = 0.0
stressBotXY: float = 0.0
stressBotYZ: float = 0.0
stressBotZX: float = 0.0
class ResultSet3D(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSet3D"):
results3D: List
class Result3D(Result, speckle_type=STRUCTURAL_RESULTS + "Result3D"):
element: Element3D = None
position: List
dispX: float = 0.0
dispY: float = 0.0
dispZ: float = 0.0
stressXX: float = 0.0
stressYY: float = 0.0
stressZZ: float = 0.0
stressXY: float = 0.0
stressYZ: float = 0.0
stressZX: float = 0.0
class ResultGlobal(Result, speckle_type=STRUCTURAL_RESULTS + "ResultGlobal"):
model: Model = None
loadX: float = 0.0
loadY: float = 0.0
loadZ: float = 0.0
loadXX: float = 0.0
loadYY: float = 0.0
loadZZ: float = 0.0
reactionX: float = 0.0
reactionY: float = 0.0
reactionZ: float = 0.0
reactionXX: float = 0.0
reactionYY: float = 0.0
reactionZZ: float = 0.0
mode: float = 0.0
frequency: float = 0.0
loadFactor: float = 0.0
modalStiffness: float = 0.0
modalGeoStiffness: float = 0.0
effMassX: float = 0.0
effMassY: float = 0.0
effMassZ: float = 0.0
effMassXX: float = 0.0
effMassYY: float = 0.0
effMassZZ: float = 0.0
class ResultSetNode(Result, speckle_type=STRUCTURAL_RESULTS + "ResultSetNode"):
resultsNode: List
class ResultNode(Result, speckle_type=STRUCTURAL_RESULTS + " ResultNode"):
node: Node = None
dispX: float = 0.0
dispY: float = 0.0
dispZ: float = 0.0
rotXX: float = 0.0
rotYY: float = 0.0
rotZZ: float = 0.0
reactionX: float = 0.0
reactionY: float = 0.0
reactionZ: float = 0.0
reactionXX: float = 0.0
reactionYY: float = 0.0
reactionZZ: float = 0.0
constraintX: float = 0.0
constraintY: float = 0.0
constraintZ: float = 0.0
constraintXX: float = 0.0
constraintYY: float = 0.0
constraintZZ: float = 0.0
velX: float = 0.0
velY: float = 0.0
velZ: float = 0.0
velXX: float = 0.0
velYY: float = 0.0
velZZ: float = 0.0
accX: float = 0.0
accY: float = 0.0
accZ: float = 0.0
accXX: float = 0.0
accYY: float = 0.0
accZZ: float = 0.0
class ResultSetAll(Base, speckle_type=None):
resultSet1D: ResultSet1D = None
resultSet2D: ResultSet2D = None
resultSet3D: ResultSet3D = None
resultsGlobal: ResultGlobal = None
resultsNode: ResultSetNode = None
+63
View File
@@ -0,0 +1,63 @@
from warnings import warn
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
UNITS = ["mm", "cm", "m", "in", "ft", "yd", "mi"]
UNITS_STRINGS = {
"mm": ["mm", "mil", "millimeters", "millimetres"],
"cm": ["cm", "centimetre", "centimeter", "centimetres", "centimeters"],
"m": ["m", "meter", "meters", "metre", "metres"],
"km": ["km", "kilometer", "kilometre", "kilometers", "kilometres"],
"in": ["in", "inch", "inches"],
"ft": ["ft", "foot", "feet"],
"yd": ["yd", "yard", "yards"],
"mi": ["mi", "mile", "miles"],
"none": ["none", "null"],
}
UNITS_ENCODINGS = {
"none": 0,
None: 0,
"mm": 1,
"cm": 2,
"m": 3,
"km": 4,
"in": 5,
"ft": 6,
"yd": 7,
"mi": 8,
}
def get_units_from_string(unit: str):
if not isinstance(unit, str):
warn(
f"Invalid units: expected type str but received {type(unit)} ({unit}). Skipping - no units will be set.",
SpeckleWarning,
)
return
unit = str.lower(unit)
for name, alternates in UNITS_STRINGS.items():
if unit in alternates:
return name
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit (eg {UNITS})."
)
def get_units_from_encoding(unit: int):
for name, encoding in UNITS_ENCODINGS.items():
if unit == encoding:
return name
raise SpeckleException(
message=f"Could not understand what unit {unit} is referring to. Please enter a valid unit encoding (eg {UNITS_ENCODINGS})."
)
def get_encoding_from_units(unit: str):
try:
return UNITS_ENCODINGS[unit]
except KeyError as e:
raise SpeckleException(message=f"No encoding exists for unit {unit}. Please enter a valid unit to encode (eg {UNITS_ENCODINGS}).") from e
@@ -1,17 +1,21 @@
import hashlib
import re import re
import warnings
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple
from uuid import uuid4
from warnings import warn
import ujson import ujson
import hashlib
import warnings
from uuid import uuid4
from enum import Enum
from warnings import warn
from typing import Any, Dict, List, Tuple
from specklepy.objects.base import Base, DataChunk
from specklepy.logging.exceptions import (
SpeckleException,
SpeckleWarning,
)
from specklepy.transports.abstract_transport import AbstractTransport
# import for serialization # import for serialization
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning import specklepy.objects.geometry
from specklepy.objects.base import Base, DataChunk import specklepy.objects.other
from specklepy.transports.abstract_transport import AbstractTransport
PRIMITIVES = (int, float, str, bool) PRIMITIVES = (int, float, str, bool)
@@ -27,8 +31,7 @@ def safe_json_loads(obj: str, obj_id=None) -> Any:
import json import json
warn( warn(
f"Failed to deserialise object (id: {obj_id}). This is likely a ujson big" f"Failed to deserialise object (id: {obj_id}). This is likely a ujson big int error - falling back to json. \nError: {err}",
f" int error - falling back to json. \nError: {err}",
SpeckleWarning, SpeckleWarning,
) )
return json.loads(obj) return json.loads(obj)
@@ -41,15 +44,9 @@ class BaseObjectSerializer:
lineage: List[str] # keeps track of hash chain through the object tree lineage: List[str] # keeps track of hash chain through the object tree
family_tree: Dict[str, Dict[str, int]] family_tree: Dict[str, Dict[str, int]]
closure_table: Dict[str, Dict[str, int]] closure_table: Dict[str, Dict[str, int]]
deserialized: Dict[ deserialized: Dict[str, Base] # holds deserialized objects so objects with same id return the same instance
str, Base
] # holds deserialized objects so objects with same id return the same instance
def __init__( def __init__(self, write_transports: List[AbstractTransport] = None, read_transport=None) -> None:
self,
write_transports: Optional[List[AbstractTransport]] = None,
read_transport: Optional[AbstractTransport] = None,
) -> None:
self.write_transports = write_transports or [] self.write_transports = write_transports or []
self.read_transport = read_transport self.read_transport = read_transport
self.detach_lineage = [] self.detach_lineage = []
@@ -64,23 +61,21 @@ class BaseObjectSerializer:
base {Base} -- the base object to be decomposed and serialized base {Base} -- the base object to be decomposed and serialized
Returns: Returns:
(str, str) -- a tuple containing the object id of the base object and (str, str) -- a tuple containing the object id of the base object and the serialized object string
the serialized object string
""" """
obj_id, obj = self.traverse_base(base) obj_id, obj = self.traverse_base(base)
return obj_id, ujson.dumps(obj) return obj_id, ujson.dumps(obj)
def traverse_base(self, base: Base) -> Tuple[str, Dict[str, Any]]: def traverse_base(self, base: Base) -> Tuple[str, Dict]:
"""Decomposes the given base object and builds a serializable dictionary """Decomposes the given base object and builds a serializable dictionary
Arguments: Arguments:
base {Base} -- the base object to be decomposed and serialized base {Base} -- the base object to be decomposed and serialized
Returns: Returns:
(str, dict) -- a tuple containing the object id of the base object and (str, dict) -- a tuple containing the object id of the base object and the constructed serializable dictionary
the constructed serializable dictionary
""" """
self.__reset_writer() self.__reset_writer()
@@ -213,8 +208,6 @@ class BaseObjectSerializer:
Returns: Returns:
Any -- a serializable version of the given object Any -- a serializable version of the given object
""" """
if obj is None:
return None
if isinstance(obj, PRIMITIVES): if isinstance(obj, PRIMITIVES):
return obj return obj
@@ -252,19 +245,16 @@ class BaseObjectSerializer:
else: else:
try: try:
return obj.dict() return obj.dict()
except Exception: except:
warn( warn(
f"Failed to handle {type(obj)} in" f"Failed to handle {type(obj)} in `BaseObjectSerializer.traverse_value`",
" `BaseObjectSerializer.traverse_value`",
SpeckleWarning, SpeckleWarning,
) )
return str(obj) return str(obj)
def detach_helper(self, ref_id: str) -> Dict[str, str]: def detach_helper(self, ref_id: str) -> Dict[str, str]:
""" """Helper to keep track of detached objects and their depth in the family tree and create reference objects to place in the parent object
Helper to keep track of detached objects and their depth in the family tree
and create reference objects to place in the parent object
Arguments: Arguments:
ref_id {str} -- the id of the fully traversed object ref_id {str} -- the id of the fully traversed object
@@ -287,10 +277,7 @@ class BaseObjectSerializer:
} }
def __reset_writer(self) -> None: def __reset_writer(self) -> None:
""" """Reinitializes the lineage, and other variables that get used during the json writing process"""
Reinitializes the lineage, and other variables that get used during the json
writing process
"""
self.detach_lineage = [True] self.detach_lineage = [True]
self.lineage = [] self.lineage = []
self.family_tree = {} self.family_tree = {}
@@ -367,8 +354,7 @@ class BaseObjectSerializer:
base.__setattr__(prop, self.recompose_base(obj=ref_obj)) base.__setattr__(prop, self.recompose_base(obj=ref_obj))
else: else:
warnings.warn( warnings.warn(
f"Could not find the referenced child object of id `{ref_id}`" f"Could not find the referenced child object of id `{ref_id}` in the given read transport: {self.read_transport.name}",
f" in the given read transport: {self.read_transport.name}",
SpeckleWarning, SpeckleWarning,
) )
base.__setattr__(prop, self.handle_value(value)) base.__setattr__(prop, self.handle_value(value))
@@ -383,8 +369,7 @@ class BaseObjectSerializer:
return base return base
def handle_value(self, obj: Any): def handle_value(self, obj: Any):
"""Helper for recomposing a base object by handling the dictionary """Helper for recomposing a base object by handling the dictionary representation's values
representation's values
Arguments: Arguments:
obj {Any} -- a value from the base object dictionary obj {Any} -- a value from the base object dictionary
@@ -430,8 +415,7 @@ class BaseObjectSerializer:
ref_obj_str = self.read_transport.get_object(id=ref_id) ref_obj_str = self.read_transport.get_object(id=ref_id)
if not ref_obj_str: if not ref_obj_str:
warnings.warn( warnings.warn(
f"Could not find the referenced child object of id `{ref_id}` in the" f"Could not find the referenced child object of id `{ref_id}` in the given read transport: {self.read_transport.name}",
f" given read transport: {self.read_transport.name}",
SpeckleWarning, SpeckleWarning,
) )
return obj return obj
@@ -1,12 +1,24 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, List, Optional from typing import Optional, List, Dict
from pydantic import BaseModel
from pydantic.main import Extra
# __________________
# | |
# | this is v wip |
# | pls be careful |
# |__________________|
# (\__/) ||
# (•ㅅ•) ||
# /  
class AbstractTransport(ABC): class AbstractTransport(ABC, BaseModel):
_name: str = "Abstract"
@property @property
@abstractmethod
def name(self): def name(self):
pass return type(self)._name
@abstractmethod @abstractmethod
def begin_write(self) -> None: def begin_write(self) -> None:
@@ -15,9 +27,7 @@ class AbstractTransport(ABC):
@abstractmethod @abstractmethod
def end_write(self) -> None: def end_write(self) -> None:
""" """Optional: signals to the transport that no more items will need to be written."""
Optional: signals to the transport that no more items will need to be written.
"""
pass pass
@abstractmethod @abstractmethod
@@ -38,8 +48,7 @@ class AbstractTransport(ABC):
Arguments: Arguments:
id {str} -- the hash of the object id {str} -- the hash of the object
source_transport {AbstractTransport) source_transport {AbstractTransport) -- the transport through which the object can be found
-- the transport through which the object can be found
""" """
pass pass
@@ -51,8 +60,7 @@ class AbstractTransport(ABC):
id {str} -- the hash of the object id {str} -- the hash of the object
Returns: Returns:
str -- the full string representation str -- the full string representation of the object (or null if no object is found)
of the object (or null if no object is found)
""" """
pass pass
@@ -64,8 +72,7 @@ class AbstractTransport(ABC):
id_list -- List of object id to be checked id_list -- List of object id to be checked
Returns: Returns:
Dict[str, bool] -- keys: input ids, values: Dict[str, bool] -- keys: input ids, values: whether the transport has that object
whether the transport has that object
""" """
pass pass
@@ -77,9 +84,12 @@ class AbstractTransport(ABC):
Arguments: Arguments:
id {str} -- the id of the object you want to copy id {str} -- the id of the object you want to copy
target_transport {AbstractTransport} target_transport {AbstractTransport} -- the transport you want to copy the object to
-- the transport you want to copy the object to
Returns: Returns:
str -- the string representation of the root object str -- the string representation of the root object
""" """
pass pass
class Config:
extra = Extra.allow
arbitrary_types_allowed = True
@@ -1,18 +1,16 @@
from typing import Dict, List from typing import Any, List, Dict
from specklepy.transports.abstract_transport import AbstractTransport from specklepy.transports.abstract_transport import AbstractTransport
class MemoryTransport(AbstractTransport): class MemoryTransport(AbstractTransport):
def __init__(self, name="Memory") -> None: _name: str = "Memory"
super().__init__() objects: dict = {}
self._name = name saved_object_count: int = 0
self.objects = {}
self.saved_object_count = 0
@property def __init__(self, name=None, **data: Any) -> None:
def name(self) -> str: super().__init__(**data)
return self._name if name:
self._name = name
def __repr__(self) -> str: def __repr__(self) -> str:
return f"MemoryTransport(objects: {len(self.objects)})" return f"MemoryTransport(objects: {len(self.objects)})"
+1
View File
@@ -0,0 +1 @@
from .server import ServerTransport
@@ -1,11 +1,10 @@
import gzip
import json import json
import logging import logging
import queue
import threading import threading
import queue
import gzip
import requests import requests
from specklepy.logging.exceptions import SpeckleException from specklepy.logging.exceptions import SpeckleException
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -18,7 +17,6 @@ class BatchSender(object):
stream_id, stream_id,
token, token,
max_batch_size_mb=1, max_batch_size_mb=1,
max_batch_length=20000,
batch_buffer_length=10, batch_buffer_length=10,
thread_count=4, thread_count=4,
): ):
@@ -27,7 +25,6 @@ class BatchSender(object):
self._token = token self._token = token
self.max_size = int(max_batch_size_mb * 1000 * 1000) self.max_size = int(max_batch_size_mb * 1000 * 1000)
self.max_batch_length = int(max_batch_length)
self._batches = queue.Queue(batch_buffer_length) self._batches = queue.Queue(batch_buffer_length)
self._crt_batch = [] self._crt_batch = []
self._crt_batch_size = 0 self._crt_batch_size = 0
@@ -41,11 +38,7 @@ class BatchSender(object):
self._create_threads() self._create_threads()
crt_obj_size = len(obj) crt_obj_size = len(obj)
crt_batch_length = len(self._crt_batch) if not self._crt_batch or self._crt_batch_size + crt_obj_size < self.max_size:
if not self._crt_batch or (
self._crt_batch_size + crt_obj_size < self.max_size
and crt_batch_length < self.max_batch_length
):
self._crt_batch.append((id, obj)) self._crt_batch.append((id, obj))
self._crt_batch_size += crt_obj_size self._crt_batch_size += crt_obj_size
return return
@@ -96,18 +89,17 @@ class BatchSender(object):
self._exception = self._exception or ex self._exception = self._exception or ex
LOG.error("ServerTransport sending thread error: " + str(ex)) LOG.error("ServerTransport sending thread error: " + str(ex))
def _bg_send_batch(self, session: requests.Session, batch): def _bg_send_batch(self, session, batch):
object_ids = [obj[0] for obj in batch] object_ids = [obj[0] for obj in batch]
response = session.post( try:
url=f"{self.server_url}/api/diff/{self.stream_id}", server_has_object = session.post(
data={"objects": json.dumps(object_ids)}, url=f"{self.server_url}/api/diff/{self.stream_id}",
) data={"objects": json.dumps(object_ids)},
if response.status_code == 403: ).json()
except Exception as ex:
raise SpeckleException( raise SpeckleException(
f"Invalid credentials - cannot send objects to server {self.server_url}" f"Invalid credentials - cannot send objects to server {self.server_url}"
) ) from ex
response.raise_for_status()
server_has_object = response.json()
new_object_ids = [x for x in object_ids if not server_has_object[x]] new_object_ids = [x for x in object_ids if not server_has_object[x]]
new_object_ids = set(new_object_ids) new_object_ids = set(new_object_ids)
@@ -115,8 +107,7 @@ class BatchSender(object):
if not new_objects: if not new_objects:
LOG.info( LOG.info(
f"Uploading batch of {len(batch)} objects: all objects are already in" f"Uploading batch of {len(batch)} objects: all objects are already in the server"
" the server"
) )
return return
@@ -135,16 +126,11 @@ class BatchSender(object):
if r.status_code != 201: if r.status_code != 201:
LOG.warning("Upload server response: %s", r.text) LOG.warning("Upload server response: %s", r.text)
raise SpeckleException( raise SpeckleException(
message=( message=f"Could not save the object to the server - status code {r.status_code}"
"Could not save the object to the server - status code"
f" {r.status_code} ({r.text[:1000]})"
)
) )
except json.JSONDecodeError as error: except json.JSONDecodeError as error:
return SpeckleException( return SpeckleException(
f"Failed to send objects to {self.server_url}. Please ensure this" f"Failed to send objects to {self.server_url}. Please ensure this stream ({self.stream_id}) exists on this server and that you have permission to send to it.",
f" stream ({self.stream_id}) exists on this server and that you have"
" permission to send to it.",
error, error,
) )
@@ -1,11 +1,11 @@
import json import json
from typing import Dict, List, Optional import requests
from warnings import warn from warnings import warn
import requests from typing import Any, Dict, List
from specklepy.core.api.client import SpeckleClient from specklepy.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_account_from_token from specklepy.api.credentials import Account, get_account_from_token
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
from specklepy.transports.abstract_transport import AbstractTransport from specklepy.transports.abstract_transport import AbstractTransport
@@ -14,10 +14,10 @@ from .batch_sender import BatchSender
class ServerTransport(AbstractTransport): class ServerTransport(AbstractTransport):
""" """
The `ServerTransport` is the vehicle through which you transport objects to and The `ServerTransport` is the vehicle through which you transport objects to and from a Speckle Server. Provide it to
from a Speckle Server. Provide it to `operations.send()` or `operations.receive()`. `operations.send()` or `operations.receive()`.
The `ServerTransport` can be authenticated two different ways: The `ServerTransport` can be authenticted two different ways:
1. by providing a `SpeckleClient` 1. by providing a `SpeckleClient`
2. by providing an `Account` 2. by providing an `Account`
3. by providing a `token` and `url` 3. by providing a `token` and `url`
@@ -29,15 +29,14 @@ class ServerTransport(AbstractTransport):
# here's the data you want to send # here's the data you want to send
block = Block(length=2, height=4) block = Block(length=2, height=4)
# next create the server transport - this is the vehicle through which # next create the server transport - this is the vehicle through which you will send and receive
# you will send and receive
transport = ServerTransport(stream_id=new_stream_id, client=client) transport = ServerTransport(stream_id=new_stream_id, client=client)
# this serialises the block and sends it to the transport # this serialises the block and sends it to the transport
hash = operations.send(base=block, transports=[transport]) hash = operations.send(base=block, transports=[transport])
# you can now create a commit on your stream with this object # you can now create a commit on your stream with this object
commit_id = client.commit.create( commid_id = client.commit.create(
stream_id=new_stream_id, stream_id=new_stream_id,
obj_id=hash, obj_id=hash,
message="this is a block I made in speckle-py", message="this is a block I made in speckle-py",
@@ -45,25 +44,28 @@ class ServerTransport(AbstractTransport):
``` ```
""" """
_name = "RemoteTransport"
url: str = None
stream_id: str = None
account: Account = None
saved_obj_count: int = 0
session: requests.Session = None
def __init__( def __init__(
self, self,
stream_id: str, stream_id: str,
client: Optional[SpeckleClient] = None, client: SpeckleClient = None,
account: Optional[Account] = None, account: Account = None,
token: Optional[str] = None, token: str = None,
url: Optional[str] = None, url: str = None,
name: str = "RemoteTransport", **data: Any,
) -> None: ) -> None:
super().__init__() super().__init__(**data)
if client is None and account is None and token is None and url is None: if client is None and account is None and token is None and url is None:
raise SpeckleException( raise SpeckleException(
"You must provide either a client or a token and url to construct a" "You must provide either a client or a token and url to construct a ServerTransport."
" ServerTransport."
) )
self._name = name
self.account = None
self.saved_obj_count = 0
if account: if account:
self.account = account self.account = account
url = account.serverInfo.url url = account.serverInfo.url
@@ -72,8 +74,7 @@ class ServerTransport(AbstractTransport):
if not client.account.token: if not client.account.token:
warn( warn(
SpeckleWarning( SpeckleWarning(
"Unauthenticated Speckle Client provided to Server Transport" f"Unauthenticated Speckle Client provided to Server Transport for {self.url}. Receiving from private streams will fail."
f" for {url}. Receiving from private streams will fail."
) )
) )
else: else:
@@ -84,22 +85,14 @@ class ServerTransport(AbstractTransport):
self.stream_id = stream_id self.stream_id = stream_id
self.url = url self.url = url
self._batch_sender = BatchSender(
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
self.session = requests.Session() self.session = requests.Session()
self.session.headers.update(
if self.account is not None: {"Authorization": f"Bearer {self.account.token}", "Accept": "text/plain"}
self._batch_sender = BatchSender( )
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
)
self.session.headers.update(
{
"Authorization": f"Bearer {self.account.token}",
"Accept": "text/plain",
}
)
@property
def name(self) -> str:
return self._name
def begin_write(self) -> None: def begin_write(self) -> None:
self.saved_obj_count = 0 self.saved_obj_count = 0
@@ -125,10 +118,8 @@ class ServerTransport(AbstractTransport):
# return obj # return obj
raise SpeckleException( raise SpeckleException(
"Getting a single object using `ServerTransport.get_object()` is not" "Getting a single object using `ServerTransport.get_object()` is not implemented. To get an object from the server, please use the `SpeckleClient.object.get()` route",
" implemented. To get an object from the server, please use the" NotImplementedError,
" `SpeckleClient.object.get()` route",
NotImplementedError(),
) )
def has_objects(self, id_list: List[str]) -> Dict[str, bool]: def has_objects(self, id_list: List[str]) -> Dict[str, bool]:
@@ -143,8 +134,7 @@ class ServerTransport(AbstractTransport):
if r.status_code != 200: if r.status_code != 200:
raise SpeckleException( raise SpeckleException(
f"Can't get object {self.stream_id}/{id}: HTTP error" f"Can't get object {self.stream_id}/{id}: HTTP error {r.status_code} ({r.text[:1000]})"
f" {r.status_code} ({r.text[:1000]})"
) )
root_obj_serialized = r.text root_obj_serialized = r.text
root_obj = json.loads(root_obj_serialized) root_obj = json.loads(root_obj_serialized)
@@ -1,30 +1,41 @@
import os import os
import sys
import time
import sched
import sqlite3 import sqlite3
from typing import Any, List, Dict, Tuple
from appdirs import user_data_dir
from contextlib import closing from contextlib import closing
from typing import Dict, List, Optional, Tuple
from specklepy.core.helpers import speckle_path_provider
from specklepy.logging.exceptions import SpeckleException
from specklepy.transports.abstract_transport import AbstractTransport from specklepy.transports.abstract_transport import AbstractTransport
from specklepy.logging.exceptions import SpeckleException
class SQLiteTransport(AbstractTransport): class SQLiteTransport(AbstractTransport):
_name = "SQLite"
_base_path: str = None
_root_path: str = None
__connection: sqlite3.Connection = None
app_name: str = ""
scope: str = ""
saved_obj_count: int = 0
max_size: int = None
_current_batch: List[Tuple[str, str]] = None
_current_batch_size: int = None
def __init__( def __init__(
self, self,
base_path: Optional[str] = None, base_path: str = None,
app_name: Optional[str] = None, app_name: str = None,
scope: Optional[str] = None, scope: str = None,
max_batch_size_mb: float = 10.0, max_batch_size_mb: float = 10.0,
name: str = "SQLite", **data: Any,
) -> None: ) -> None:
super().__init__() super().__init__(**data)
self._name = name
self.app_name = app_name or "Speckle" self.app_name = app_name or "Speckle"
self.scope = scope or "Objects" self.scope = scope or "Objects"
self._base_path = base_path or self.get_base_path(self.app_name) self._base_path = base_path or self.get_base_path(self.app_name)
self.max_size = int(max_batch_size_mb * 1000 * 1000) self.max_size = int(max_batch_size_mb * 1000 * 1000)
self.saved_obj_count = 0 self._current_batch = []
self._current_batch: List[Tuple[str, str]] = []
self._current_batch_size = 0 self._current_batch_size = 0
try: try:
@@ -36,24 +47,30 @@ class SQLiteTransport(AbstractTransport):
self.__initialise() self.__initialise()
except Exception as ex: except Exception as ex:
raise SpeckleException( raise SpeckleException(
f"SQLiteTransport could not initialise {self.scope}.db at" f"SQLiteTransport could not initialise {self.scope}.db at {self._base_path}. Either provide a different `base_path` or use an alternative transport.",
f" {self._base_path}. Either provide a different `base_path` or use an"
" alternative transport.",
ex, ex,
) )
def __repr__(self) -> str: def __repr__(self) -> str:
return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')" return f"SQLiteTransport(app: '{self.app_name}', scope: '{self.scope}')"
@property
def name(self) -> str:
return self._name
@staticmethod @staticmethod
def get_base_path(app_name): def get_base_path(app_name):
return str( # from appdirs https://github.com/ActiveState/appdirs/blob/master/appdirs.py
speckle_path_provider.user_application_data_path().joinpath(app_name) # default mac path is not the one we use (we use unix path), so using special case for this
) system = sys.platform
if system.startswith("java"):
import platform
os_name = platform.java_ver()[3][0]
if os_name.startswith("Mac"):
system = "darwin"
if system != "darwin":
return user_data_dir(appname=app_name, appauthor=False, roaming=True)
path = os.path.expanduser("~/.config/")
return os.path.join(path, app_name)
def save_object_from_transport( def save_object_from_transport(
self, id: str, source_transport: AbstractTransport self, id: str, source_transport: AbstractTransport
@@ -62,16 +79,14 @@ class SQLiteTransport(AbstractTransport):
Arguments: Arguments:
id {str} -- the object id id {str} -- the object id
source_transport {AbstractTransport) source_transport {AbstractTransport) -- the transport through which the object can be found
-- the transport through which the object can be found
""" """
serialized_object = source_transport.get_object(id) serialized_object = source_transport.get_object(id)
self.save_object(id, serialized_object) self.save_object(id, serialized_object)
def save_object(self, id: str, serialized_object: str) -> None: def save_object(self, id: str, serialized_object: str) -> None:
""" """
Adds an object to the current batch to be written to the db. Adds an object to the current batch to be written to the db. If the current batch is full,
If the current batch is full,
the batch is written to the db and the current batch is reset. the batch is written to the db and the current batch is reset.
Arguments: Arguments:
@@ -103,8 +118,7 @@ class SQLiteTransport(AbstractTransport):
self.__connection.commit() self.__connection.commit()
except Exception as ex: except Exception as ex:
raise SpeckleException( raise SpeckleException(
"Could not save the batch of objects to the local db. Inner exception:" f"Could not save the batch of objects to the local db. Inner exception: {ex}",
f" {ex}",
ex, ex,
) )
@@ -143,10 +157,7 @@ class SQLiteTransport(AbstractTransport):
raise NotImplementedError raise NotImplementedError
def get_all_objects(self): def get_all_objects(self):
""" """Returns all the objects in the store. NOTE: do not use for large collections!"""
Returns all the objects in the store.
NOTE: do not use for large collections!
"""
self.__check_connection() self.__check_connection()
with closing(self.__connection.cursor()) as c: with closing(self.__connection.cursor()) as c:
rows = c.execute("SELECT * FROM objects").fetchall() rows = c.execute("SELECT * FROM objects").fetchall()
-24
View File
@@ -1,24 +0,0 @@
"""This module contains an SDK for working with Speckle Automate."""
from speckle_automate.automation_context import AutomationContext
from speckle_automate.runner import execute_automate_function, run_function
from speckle_automate.schema import (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResultLevel,
ResultCase,
)
__all__ = [
"AutomationContext",
"AutomateBase",
"AutomationStatus",
"AutomationResult",
"AutomationRunData",
"ResultCase",
"ObjectResultLevel",
"run_function",
"execute_automate_function",
]
-427
View File
@@ -1,427 +0,0 @@
"""This module provides an abstraction layer above the Speckle Automate runtime."""
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple, Union
import httpx
from gql import gql
from speckle_automate.schema import (
AutomateBase,
AutomationResult,
AutomationRunData,
AutomationStatus,
ObjectResultLevel,
ResultCase,
)
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.core.api.models import Branch
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects.base import Base
from specklepy.transports.memory import MemoryTransport
from specklepy.transports.server import ServerTransport
@dataclass
class AutomationContext:
"""A context helper class.
This class exposes methods to work with the Speckle Automate context inside
Speckle Automate functions.
An instance of AutomationContext is injected into every run of a function.
"""
automation_run_data: AutomationRunData
speckle_client: SpeckleClient
_server_transport: ServerTransport
_speckle_token: str
#: keep a memory transponrt at hand, to speed up things if needed
_memory_transport: MemoryTransport = field(default_factory=MemoryTransport)
#: added for performance measuring
_init_time: float = field(default_factory=time.perf_counter)
_automation_result: AutomationResult = field(default_factory=AutomationResult)
@classmethod
def initialize(
cls, automation_run_data: Union[str, AutomationRunData], speckle_token: str
) -> "AutomationContext":
"""Bootstrap the AutomateSDK from raw data.
Todo:
----
* bootstrap a structlog logger instance
* expose a logger, that ppl can use instead of print
"""
# parse the json value if its not an initialized project data instance
automation_run_data = (
automation_run_data
if isinstance(automation_run_data, AutomationRunData)
else AutomationRunData.model_validate_json(automation_run_data)
)
speckle_client = SpeckleClient(
automation_run_data.speckle_server_url,
automation_run_data.speckle_server_url.startswith("https"),
)
speckle_client.authenticate_with_token(speckle_token)
if not speckle_client.account:
msg = (
f"Could not autenticate to {automation_run_data.speckle_server_url}",
"with the provided token",
)
raise ValueError(msg)
server_transport = ServerTransport(
automation_run_data.project_id, speckle_client
)
return cls(automation_run_data, speckle_client, server_transport, speckle_token)
@property
def run_status(self) -> AutomationStatus:
"""Get the status of the automation run."""
return self._automation_result.run_status
@property
def status_message(self) -> Optional[str]:
"""Get the current status message."""
return self._automation_result.status_message
def elapsed(self) -> float:
"""Return the elapsed time in seconds since the initialization time."""
return time.perf_counter() - self._init_time
def receive_version(self) -> Base:
"""Receive the Speckle project version that triggered this automation run."""
# TODO: this is a quick hack to keep implementation consistency. Move to proper receive many versions
version_id = self.automation_run_data.triggers[0].payload.version_id
commit = self.speckle_client.commit.get(
self.automation_run_data.project_id, version_id
)
if not commit.referencedObject:
raise ValueError("The commit has no referencedObject, cannot receive it.")
base = operations.receive(
commit.referencedObject, self._server_transport, self._memory_transport
)
print(
f"It took {self.elapsed():.2f} seconds to receive",
f" the speckle version {version_id}",
)
return base
def create_new_version_in_project(
self, root_object: Base, model_name: str, version_message: str = ""
) -> Tuple[str, str]:
"""Save a base model to a new version on the project.
Args:
root_object (Base): The Speckle base object for the new version.
model_id (str): For now please use a `branchName`!
version_message (str): The message for the new version.
"""
branch = self.speckle_client.branch.get(
self.automation_run_data.project_id, model_name, 1
)
if isinstance(branch, Branch):
if not branch.id:
raise ValueError("Cannot use the branch without its id")
matching_trigger = [
t
for t in self.automation_run_data.triggers
if t.payload.model_id == branch.id
]
if matching_trigger:
raise ValueError(
f"The target model: {model_name} cannot match the model"
f" that triggered this automation:"
f" {matching_trigger[0].payload.model_id}"
)
model_id = branch.id
else:
# we just check if it exists
branch_create = self.speckle_client.branch.create(
self.automation_run_data.project_id,
model_name,
)
if isinstance(branch_create, Exception):
raise branch_create
model_id = branch_create
root_object_id = operations.send(
root_object,
[self._server_transport, self._memory_transport],
use_default_cache=False,
)
version_id = self.speckle_client.commit.create(
stream_id=self.automation_run_data.project_id,
object_id=root_object_id,
branch_name=model_name,
message=version_message,
source_application="SpeckleAutomate",
)
if isinstance(version_id, SpeckleException):
raise version_id
self._automation_result.result_versions.append(version_id)
return model_id, version_id
@property
def context_view(self) -> Optional[str]:
return self._automation_result.result_view
def set_context_view(
self,
# f"{model_id}@{version_id} or {model_id} "
resource_ids: Optional[List[str]] = None,
include_source_model_version: bool = True,
) -> None:
link_resources = (
[
f"{t.payload.model_id}@{t.payload.version_id}"
for t in self.automation_run_data.triggers
]
if include_source_model_version
else []
)
if resource_ids:
link_resources.extend(resource_ids)
if not link_resources:
raise Exception(
"We do not have enough resource ids to compose a context view"
)
self._automation_result.result_view = (
f"/projects/{self.automation_run_data.project_id}"
f"/models/{','.join(link_resources)}"
)
def report_run_status(self) -> None:
"""Report the current run status to the project of this automation."""
query = gql(
"""
mutation AutomateFunctionRunStatusReport(
$projectId: String!
$functionRunId: String!
$status: AutomateRunStatus!
$statusMessage: String
$results: JSONObject
$contextView: String
){
automateFunctionRunStatusReport(input: {
projectId: $projectId
functionRunId: $functionRunId
status: $status
statusMessage: $statusMessage
contextView: $contextView
results: $results
})
}
"""
)
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
object_results = {
"version": 1,
"values": {
"objectResults": self._automation_result.model_dump(by_alias=True)[
"objectResults"
],
"blobIds": self._automation_result.blobs,
},
}
else:
object_results = None
params = {
"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,
"results": object_results,
"contextView": self._automation_result.result_view,
}
print(f"Reporting run status with content: {params}")
self.speckle_client.httpclient.execute(query, params)
def store_file_result(self, file_path: Union[Path, str]) -> None:
"""Save a file attached to the project of this automation."""
path_obj = (
Path(file_path).resolve() if isinstance(file_path, str) else file_path
)
class UploadResult(AutomateBase):
blob_id: str
file_name: str
upload_status: int
class BlobUploadResponse(AutomateBase):
upload_results: list[UploadResult]
if not path_obj.exists():
raise ValueError("The given file path doesn't exist")
files = {path_obj.name: open(str(path_obj), "rb")}
url = (
f"{self.automation_run_data.speckle_server_url}api/stream/"
f"{self.automation_run_data.project_id}/blob"
)
data = (
httpx.post(
url,
files=files,
headers={"authorization": f"Bearer {self._speckle_token}"},
)
.raise_for_status()
.json()
)
upload_response = BlobUploadResponse.model_validate(data)
if len(upload_response.upload_results) != 1:
raise ValueError("Expecting one upload result.")
self._automation_result.blobs.extend(
[upload_result.blob_id for upload_result in upload_response.upload_results]
)
def mark_run_failed(self, status_message: str) -> None:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.FAILED, status_message)
def mark_run_exception(self, status_message: str) -> None:
"""Mark the current run a failure."""
self._mark_run(AutomationStatus.EXCEPTION, status_message)
def mark_run_success(self, status_message: Optional[str]) -> None:
"""Mark the current run a success with an optional message."""
self._mark_run(AutomationStatus.SUCCEEDED, status_message)
def _mark_run(
self, status: AutomationStatus, status_message: Optional[str]
) -> None:
duration = self.elapsed()
self._automation_result.status_message = status_message
self._automation_result.run_status = status
self._automation_result.elapsed = duration
msg = f"Automation run {status.value} after {duration:.2f} seconds."
print("\n".join([msg, status_message]) if status_message else msg)
def attach_error_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new error case to the run results.
If the error cause has already created an error case,
the error will be extended with a new case refering to the causing objects.
Args:
error_tag (str): A short tag for the error type.
causing_object_ids (str[]): A list of object_id-s that are causing the error
error_messagge (Optional[str]): Optional error message.
metadata: User provided metadata key value pairs
visual_overrides: Case specific 3D visual overrides.
"""
self.attach_result_to_objects(
ObjectResultLevel.ERROR,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_warning_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new warning case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.WARNING,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_success_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new success case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.SUCCESS,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_info_to_objects(
self,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
"""Add a new info case to the run results."""
self.attach_result_to_objects(
ObjectResultLevel.INFO,
category,
object_ids,
message,
metadata,
visual_overrides,
)
def attach_result_to_objects(
self,
level: ObjectResultLevel,
category: str,
object_ids: Union[str, List[str]],
message: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
visual_overrides: Optional[Dict[str, Any]] = None,
) -> None:
if isinstance(object_ids, list):
if len(object_ids) < 1:
raise ValueError(
f"Need atleast one object_id to report a(n) {level.value.upper()}"
)
id_list = object_ids
else:
id_list = [object_ids]
print(
f"Created new {level.value.upper()}"
f" category: {category} caused by: {message}"
)
self._automation_result.object_results.append(
ResultCase(
category=category,
level=level,
object_ids=id_list,
message=message,
metadata=metadata,
visual_overrides=visual_overrides,
)
)
-154
View File
@@ -1,154 +0,0 @@
"""Some useful helpers for working with automation data."""
import secrets
import string
import pytest
from gql import gql
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from speckle_automate.schema import AutomationRunData, TestAutomationRunData
from specklepy.api.client import SpeckleClient
class TestAutomationEnvironment(BaseSettings):
"""Get known environment variables from local `.env` file"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
env_prefix="speckle_",
extra="ignore",
)
token: str = Field()
server_url: str = Field()
project_id: str = Field()
automation_id: str = Field()
@pytest.fixture()
def test_automation_environment() -> TestAutomationEnvironment:
return TestAutomationEnvironment()
@pytest.fixture()
def test_automation_token(
test_automation_environment: TestAutomationEnvironment,
) -> str:
"""Provide a speckle token for the test suite."""
return test_automation_environment.token
@pytest.fixture()
def speckle_client(
test_automation_environment: TestAutomationEnvironment,
) -> SpeckleClient:
"""Initialize a SpeckleClient for testing."""
speckle_client = SpeckleClient(
test_automation_environment.server_url,
test_automation_environment.server_url.startswith("https"),
)
speckle_client.authenticate_with_token(test_automation_environment.token)
return speckle_client
def create_test_automation_run(
speckle_client: SpeckleClient, project_id: str, test_automation_id: str
) -> TestAutomationRunData:
"""Create test run to report local test results to"""
query = gql(
"""
mutation CreateTestRun(
$projectId: ID!,
$automationId: ID!
) {
projectMutations {
automationMutations(projectId: $projectId) {
createTestAutomationRun(automationId: $automationId) {
automationRunId
functionRunId
triggers {
payload {
modelId
versionId
}
triggerType
}
}
}
}
}
"""
)
params = {"automationId": test_automation_id, "projectId": project_id}
result = speckle_client.httpclient.execute(query, params)
print(result)
return (
result.get("projectMutations")
.get("automationMutations")
.get("createTestAutomationRun")
)
@pytest.fixture()
def test_automation_run(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> TestAutomationRunData:
return create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
def create_test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
"""Create automation run data for a new run for a given test automation"""
test_automation_run_data = create_test_automation_run(
speckle_client,
test_automation_environment.project_id,
test_automation_environment.automation_id,
)
return AutomationRunData(
project_id=test_automation_environment.project_id,
speckle_server_url=test_automation_environment.server_url,
automation_id=test_automation_environment.automation_id,
automation_run_id=test_automation_run_data["automationRunId"],
function_run_id=test_automation_run_data["functionRunId"],
triggers=test_automation_run_data["triggers"],
)
@pytest.fixture()
def test_automation_run_data(
speckle_client: SpeckleClient,
test_automation_environment: TestAutomationEnvironment,
) -> AutomationRunData:
return create_test_automation_run_data(speckle_client, test_automation_environment)
def crypto_random_string(length: int) -> str:
"""Generate a semi crypto random string of a given length."""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length)).lower()
__all__ = [
"test_automation_environment",
"test_automation_token",
"speckle_client",
"test_automation_run",
"test_automation_run_data",
]
-197
View File
@@ -1,197 +0,0 @@
"""Function execution module.
Provides mechanisms to execute any function,
that conforms to the AutomateFunction "interface"
"""
import json
import sys
import traceback
from pathlib import Path
from typing import Callable, Optional, Tuple, TypeVar, Union, overload
from pydantic import create_model
from pydantic.json_schema import GenerateJsonSchema
from speckle_automate.automation_context import AutomationContext
from speckle_automate.schema import AutomateBase, AutomationRunData, AutomationStatus
T = TypeVar("T", bound=AutomateBase)
AutomateFunction = Callable[[AutomationContext, T], None]
AutomateFunctionWithoutInputs = Callable[[AutomationContext], None]
def _read_input_data(inputs_location: str) -> str:
input_path = Path(inputs_location)
if not input_path.exists():
raise ValueError(f"Cannot find the function inputs file at {input_path}")
return input_path.read_text()
def _parse_input_data(
input_location: str, input_schema: Optional[type[T]]
) -> Tuple[AutomationRunData, Optional[T], str]:
input_json_string = _read_input_data(input_location)
class FunctionRunData(AutomateBase):
speckle_token: str
automation_run_data: AutomationRunData
function_inputs: None = None
parser_model = FunctionRunData
if input_schema:
parser_model = create_model(
"FunctionRunDataWithInputs",
function_inputs=(input_schema, ...),
__base__=FunctionRunData,
)
input_data = parser_model.model_validate_json(input_json_string)
return (
input_data.automation_run_data,
input_data.function_inputs,
input_data.speckle_token,
)
@overload
def execute_automate_function(
automate_function: AutomateFunction[T],
input_schema: type[T],
) -> None:
...
@overload
def execute_automate_function(
automate_function: AutomateFunctionWithoutInputs,
) -> None:
...
class AutomateGenerateJsonSchema(GenerateJsonSchema):
def generate(self, schema, mode="validation"):
json_schema = super().generate(schema, mode=mode)
json_schema["$schema"] = self.schema_dialect
return json_schema
def execute_automate_function(
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
input_schema: Optional[type[T]] = None,
):
"""Runs the provided automate function with the input schema."""
# first arg is the python file name, we do not need that
args = sys.argv[1:]
if len(args) != 2:
raise ValueError("Incorrect number of arguments specified need 2")
# we rely on a command name convention to decide what to do.
# this is here, so that the function authors do not see any of this
command, argument = args
if command == "generate_schema":
path = Path(argument)
schema = json.dumps(
input_schema.model_json_schema(
by_alias=True, schema_generator=AutomateGenerateJsonSchema
)
if input_schema
else {}
)
path.write_text(schema)
elif command == "run":
automation_run_data, function_inputs, speckle_token = _parse_input_data(
argument, input_schema
)
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
if function_inputs:
automation_context = run_function(
automation_context,
automate_function, # type: ignore
function_inputs, # type: ignore
)
else:
automation_context = AutomationContext.initialize(
automation_run_data, speckle_token
)
automation_context = run_function(
automation_context,
automate_function, # type: ignore
)
# if we've gotten this far, the execution should technically be completed as expected
# thus exiting with 0 is the schemantically correct thing to do
exit_code = (
1 if automation_context.run_status == AutomationStatus.EXCEPTION else 0
)
exit(exit_code)
else:
raise NotImplementedError(f"Command: '{command}' is not supported.")
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunction[T],
inputs: T,
) -> AutomationContext:
...
@overload
def run_function(
automation_context: AutomationContext,
automate_function: AutomateFunctionWithoutInputs,
) -> AutomationContext:
...
def run_function(
automation_context: AutomationContext,
automate_function: Union[AutomateFunction[T], AutomateFunctionWithoutInputs],
inputs: Optional[T] = None,
) -> AutomationContext:
"""Run the provided function with the automate sdk context."""
automation_context.report_run_status()
try:
# avoiding complex type gymnastics here on the internals.
# the external type overloads make this correct
if inputs:
automate_function(automation_context, inputs) # type: ignore
else:
automate_function(automation_context) # type: ignore
# the function author forgot to mark the function success
if automation_context.run_status not in [
AutomationStatus.FAILED,
AutomationStatus.SUCCEEDED,
AutomationStatus.EXCEPTION,
]:
automation_context.mark_run_success(
"WARNING: Automate assumed a success status,"
" but it was not marked as so by the function."
)
except Exception:
trace = traceback.format_exc()
print(trace)
automation_context.mark_run_exception(
"Function error. Check the automation run logs for details."
)
finally:
if not automation_context.context_view:
automation_context.set_context_view()
automation_context.report_run_status()
return automation_context
-98
View File
@@ -1,98 +0,0 @@
""""""
from enum import Enum
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field
from stringcase import camelcase
class AutomateBase(BaseModel):
"""Use this class as a base model for automate related DTO."""
model_config = ConfigDict(alias_generator=camelcase, populate_by_name=True)
class VersionCreationTriggerPayload(AutomateBase):
"""Represents the version creation trigger payload."""
model_id: str
version_id: str
class VersionCreationTrigger(AutomateBase):
"""Represents a single version creation trigger for the automation run."""
trigger_type: Literal["versionCreation"]
payload: VersionCreationTriggerPayload
class AutomationRunData(BaseModel):
"""Values of the project / model that triggered the run of this function."""
project_id: str
speckle_server_url: str
automation_id: str
automation_run_id: str
function_run_id: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
class TestAutomationRunData(BaseModel):
"""Values of the run created in the test automation for local test results."""
automation_run_id: str
function_run_id: str
triggers: List[VersionCreationTrigger]
model_config = ConfigDict(
alias_generator=camelcase, populate_by_name=True, protected_namespaces=()
)
class AutomationStatus(str, Enum):
"""Set the status of the automation."""
INITIALIZING = "INITIALIZING"
RUNNING = "RUNNING"
FAILED = "FAILED"
SUCCEEDED = "SUCCEEDED"
EXCEPTION = "EXCEPTION"
class ObjectResultLevel(str, Enum):
"""Possible status message levels for object reports."""
SUCCESS = "SUCCESS"
INFO = "INFO"
WARNING = "WARNING"
ERROR = "ERROR"
class ResultCase(AutomateBase):
"""A result case."""
category: str
level: ObjectResultLevel
object_ids: List[str]
message: Optional[str]
metadata: Optional[Dict[str, Any]]
visual_overrides: Optional[Dict[str, Any]]
class AutomationResult(AutomateBase):
"""Schema accepted by the Speckle server as a result for an automation run."""
elapsed: float = 0
result_view: Optional[str] = None
result_versions: List[str] = Field(default_factory=list)
blobs: List[str] = Field(default_factory=list)
run_status: AutomationStatus = AutomationStatus.RUNNING
status_message: Optional[str] = None
object_results: list[ResultCase] = Field(default_factory=list)
-3
View File
@@ -1,3 +0,0 @@
from specklepy import objects
__all__ = ["objects"]
-198
View File
@@ -1,198 +0,0 @@
from deprecated import deprecated
from specklepy.api.credentials import Account
from specklepy.api.resources import (
ActiveUserResource,
ModelResource,
OtherUserResource,
ProjectInviteResource,
ProjectResource,
SubscriptionResource,
VersionResource,
branch,
commit,
object,
server,
stream,
subscriptions,
user,
)
from specklepy.core.api.client import SpeckleClient as CoreSpeckleClient
from specklepy.logging import metrics
class SpeckleClient(CoreSpeckleClient):
"""
The `SpeckleClient` is your entry point for interacting with
your Speckle Server's GraphQL API.
You'll need to have access to a server to use it,
or you can use our public server `app.speckle.systems`.
To authenticate the client, you'll need to have downloaded
the [Speckle Manager](https://speckle.guide/#speckle-manager)
and added your account.
```py
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account
# initialise the client
client = SpeckleClient(host="app.speckle.systems") # or whatever your host is
# client = SpeckleClient(host="localhost:3000", use_ssl=False) or use local server
# authenticate the client with an account (account has been added in Speckle Manager)
account = get_default_account()
client.authenticate_with_account(account)
# create a new stream. this returns the stream id
new_stream_id = client.stream.create(name="a shiny new stream")
# use that stream id to get the stream from the server
new_stream = client.stream.get(id=new_stream_id)
```
"""
DEFAULT_HOST = "app.speckle.systems"
USE_SSL = True
def __init__(
self,
host: str = DEFAULT_HOST,
use_ssl: bool = USE_SSL,
verify_certificate: bool = True,
) -> None:
super().__init__(
host=host,
use_ssl=use_ssl,
verify_certificate=verify_certificate,
)
self.account = Account()
def _init_resources(self) -> None:
self.server = server.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
server_version = None
try:
server_version = self.server.version()
except Exception:
pass
self.other_user = OtherUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.active_user = ActiveUserResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project = ProjectResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.project_invite = ProjectInviteResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.model = ModelResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.version = VersionResource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.subscription = SubscriptionResource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
# todo: why doesn't this take a server version
)
# Deprecated Resources
self.user = user.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.stream = stream.Resource(
account=self.account,
basepath=self.url,
client=self.httpclient,
server_version=server_version,
)
self.commit = commit.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.branch = branch.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.object = object.Resource(
account=self.account, basepath=self.url, client=self.httpclient
)
self.subscribe = subscriptions.Resource(
account=self.account,
basepath=self.ws_url,
client=self.wsclient,
)
@deprecated(
version="2.6.0",
reason=(
"Renamed: please use `authenticate_with_account` or"
" `authenticate_with_token` instead."
),
)
def authenticate(self, token: str) -> None:
"""Authenticate the client using a personal access token
The token is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments:
token {str} -- an api token
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate_deprecated"}
)
return super().authenticate(token)
def authenticate_with_token(self, token: str) -> None:
"""
Authenticate the client using a personal access token.
The token is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments:
token {str} -- an api token
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate With Token"}
)
return super().authenticate_with_token(token)
def authenticate_with_account(self, account: Account) -> None:
"""Authenticate the client using an Account object
The account is saved in the client object and a synchronous GraphQL
entrypoint is created
Arguments:
account {Account} -- the account object which can be found with
`get_default_account` or `get_local_accounts`
"""
metrics.track(
metrics.SDK, self.account, {"name": "Client Authenticate With Account"}
)
return super().authenticate_with_account(account)
-73
View File
@@ -1,73 +0,0 @@
from typing import List, Optional
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.credentials import StreamWrapper # noqa: F401
from specklepy.core.api.credentials import Account, UserInfo # noqa: F401
from specklepy.core.api.credentials import (
get_account_from_token as core_get_account_from_token,
)
from specklepy.core.api.credentials import get_local_accounts as core_get_local_accounts
from specklepy.logging import metrics
def get_local_accounts(base_path: Optional[str] = None) -> List[Account]:
"""Gets all the accounts present in this environment
Arguments:
base_path {str} -- custom base path if you are not using the system default
Returns:
List[Account] -- list of all local accounts or an empty list if
no accounts were found
"""
accounts = core_get_local_accounts(base_path)
metrics.track(
metrics.SDK,
next(
(acc for acc in accounts if acc.isDefault),
accounts[0] if accounts else None,
),
{"name": "Get Local Accounts"},
)
return accounts
def get_default_account(base_path: Optional[str] = None) -> Optional[Account]:
"""
Gets this environment's default account if any. If there is no default,
the first found will be returned and set as default.
Arguments:
base_path {str} -- custom base path if you are not using the system default
Returns:
Account -- the default account or None if no local accounts were found
"""
accounts = core_get_local_accounts(base_path=base_path)
if not accounts:
return None
default = next((acc for acc in accounts if acc.isDefault), None)
if not default:
default = accounts[0]
default.isDefault = True
metrics.initialise_tracker(default)
return default
def get_account_from_token(token: str, server_url: str = None) -> Account:
"""Gets the local account for the token if it exists
Arguments:
token {str} -- the api token
Returns:
Account -- the local account with this token or a shell account containing
just the token and url if no local account is found
"""
account = core_get_account_from_token(token, server_url)
metrics.track(metrics.SDK, account, {"name": "Get Account From Token"})
return account
-74
View File
@@ -1,74 +0,0 @@
from specklepy.core.api.host_applications import (
ARCGIS,
ARCHICAD,
AUTOCAD,
BLENDER,
CIVIL,
CSIBRIDGE,
DXF,
DYNAMO,
ETABS,
EXCEL,
GRASSHOPPER,
GSA,
MICROSTATION,
NET,
OPENBUILDINGS,
OPENRAIL,
OPENROADS,
OTHER,
POWERBI,
PYTHON,
QGIS,
REVIT,
RHINO,
SAFE,
SAP2000,
SKETCHUP,
TEKLASTRUCTURES,
TOPSOLID,
UNITY,
UNREAL,
HostApplication,
HostAppVersion,
_app_name_host_app_mapping,
get_host_app_from_string,
)
# re-exporting stuff from the moved api module
__all__ = [
"ARCGIS",
"ARCHICAD",
"AUTOCAD",
"BLENDER",
"CIVIL",
"CSIBRIDGE",
"DXF",
"DYNAMO",
"ETABS",
"EXCEL",
"GRASSHOPPER",
"GSA",
"MICROSTATION",
"NET",
"OPENBUILDINGS",
"OPENRAIL",
"OPENROADS",
"OTHER",
"POWERBI",
"PYTHON",
"QGIS",
"REVIT",
"RHINO",
"SAFE",
"SAP2000",
"SKETCHUP",
"TEKLASTRUCTURES",
"TOPSOLID",
"UNITY",
"UNREAL",
"HostApplication",
"HostAppVersion",
"_app_name_host_app_mapping",
"get_host_app_from_string",
]
-35
View File
@@ -1,35 +0,0 @@
# following imports seem to be unnecessary, but they need to stay
# to not break the scripts using these functions as non-core
from specklepy.core.api.models import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
LimitedUser,
Object,
PendingStreamCollaborator,
ServerInfo,
Stream,
Streams,
User,
)
__all__ = [
"Activity",
"ActivityCollection",
"Branch",
"Branches",
"Collaborator",
"Commit",
"Commits",
"LimitedUser",
"Object",
"PendingStreamCollaborator",
"ServerInfo",
"Stream",
"Streams",
"User",
]
-97
View File
@@ -1,97 +0,0 @@
from typing import List, Optional
from specklepy.core.api.operations import deserialize as core_deserialize
from specklepy.core.api.operations import receive as _untracked_receive
from specklepy.core.api.operations import send as core_send
from specklepy.core.api.operations import serialize as core_serialize
from specklepy.logging import metrics
from specklepy.objects.base import Base
from specklepy.transports.abstract_transport import AbstractTransport
def send(
base: Base,
transports: Optional[List[AbstractTransport]] = None,
use_default_cache: bool = True,
):
"""Sends an object via the provided transports. Defaults to the local cache.
Arguments:
obj {Base} -- the object you want to send
transports {list} -- where you want to send them
use_default_cache {bool} -- toggle for the default cache.
If set to false, it will only send to the provided transports
Returns:
str -- the object id of the sent object
"""
if transports is None:
metrics.track(metrics.SEND)
else:
metrics.track(metrics.SEND, getattr(transports[0], "account", None))
return core_send(base, transports, use_default_cache)
def receive(
obj_id: str,
remote_transport: Optional[AbstractTransport] = None,
local_transport: Optional[AbstractTransport] = None,
) -> Base:
"""Receives an object from a transport.
Arguments:
obj_id {str} -- the id of the object to receive
remote_transport {Transport} -- the transport to receive from
local_transport {Transport} -- the local cache to check for existing objects
(defaults to `SQLiteTransport`)
Returns:
Base -- the base object
"""
metrics.track(metrics.RECEIVE, getattr(remote_transport, "account", None))
return _untracked_receive(obj_id, remote_transport, local_transport)
def serialize(base: Base, write_transports: List[AbstractTransport] = []) -> str:
"""
Serialize a base object. If no write transports are provided,
the object will be serialized
without detaching or chunking any of the attributes.
Arguments:
base {Base} -- the object to serialize
write_transports {List[AbstractTransport]}
-- optional: the transports to write to
Returns:
str -- the serialized object
"""
metrics.track(metrics.SDK, custom_props={"name": "Serialize"})
return core_serialize(base, write_transports)
def deserialize(
obj_string: str, read_transport: Optional[AbstractTransport] = None
) -> Base:
"""
Deserialize a string object into a Base object.
If the object contains referenced child objects that are not stored in the local db,
a read transport needs to be provided in order to recompose
the base with the children objects.
Arguments:
obj_string {str} -- the string object to deserialize
read_transport {AbstractTransport}
-- the transport to fetch children objects from
(defaults to SQLiteTransport)
Returns:
Base -- the deserialized object
"""
metrics.track(metrics.SDK, custom_props={"name": "Deserialize"})
return core_deserialize(obj_string, read_transport)
__all__ = ["receive", "send", "serialize", "deserialize"]
-24
View File
@@ -1,24 +0,0 @@
from typing import Any, Optional, Tuple
from gql.client import Client
from specklepy.api.credentials import Account
from specklepy.core.api.resource import ResourceBase as CoreResourceBase
class ResourceBase(CoreResourceBase):
def __init__(
self,
account: Account,
basepath: str,
client: Client,
name: str,
server_version: Optional[Tuple[Any, ...]] = None,
) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
name=name,
server_version=server_version,
)
-41
View File
@@ -1,41 +0,0 @@
from specklepy.api.resources.current.active_user_resource import ActiveUserResource
from specklepy.api.resources.current.model_resource import ModelResource
from specklepy.api.resources.current.other_user_resource import OtherUserResource
from specklepy.api.resources.current.project_invite_resource import (
ProjectInviteResource,
)
from specklepy.api.resources.current.project_resource import ProjectResource
from specklepy.api.resources.current.server_resource import ServerResource
from specklepy.api.resources.current.subscription_resource import SubscriptionResource
from specklepy.api.resources.current.version_resource import VersionResource
from specklepy.api.resources.deprecated import (
active_user,
branch,
commit,
object,
other_user,
server,
stream,
subscriptions,
user,
)
__all__ = [
"ActiveUserResource",
"ModelResource",
"OtherUserResource",
"ProjectInviteResource",
"ProjectResource",
"ServerResource",
"SubscriptionResource",
"VersionResource",
"active_user",
"branch",
"commit",
"object",
"other_user",
"server",
"stream",
"subscriptions",
"user",
]
@@ -1,147 +0,0 @@
from datetime import datetime
from typing import List, Optional, overload
from deprecated import deprecated
from specklepy.core.api.inputs.project_inputs import UserProjectsFilter
from specklepy.core.api.inputs.user_inputs import UserUpdateInput
from specklepy.core.api.models import (
PendingStreamCollaborator,
Project,
ResourceCollection,
User,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import ActiveUserResource as CoreResource
from specklepy.logging import metrics
class ActiveUserResource(CoreResource):
"""API Access class for users. This class provides methods to get and update
the user profile, fetch user activity, and manage pending stream invitations."""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = User
def get(self) -> Optional[User]:
metrics.track(metrics.SDK, self.account, {"name": "Active User Get"})
return super().get()
@deprecated("Use UserUpdateInput overload", version=FE1_DEPRECATION_VERSION)
@overload
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
) -> User:
...
@overload
def update(self, *, input: UserUpdateInput) -> User:
...
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
*,
input: Optional[UserUpdateInput] = None,
) -> User:
metrics.track(metrics.SDK, self.account, {"name": "Active User Update"})
if isinstance(input, UserUpdateInput):
return super()._update(input=input)
else:
return super()._update(
input=UserUpdateInput(
name=name,
company=company,
bio=bio,
avatar=avatar,
)
)
def get_projects(
self,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[UserProjectsFilter] = None,
) -> ResourceCollection[Project]:
metrics.track(metrics.SDK, self.account, {"name": "Active User Get Projects"})
return super().get_projects(limit=limit, cursor=cursor, filter=filter)
def get_project_invites(self) -> List[PendingStreamCollaborator]:
metrics.track(
metrics.SDK, self.account, {"name": "Active User Get Project Invites"}
)
return super().get_project_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Fetches collection the current authenticated user's activity
as filtered by given parameters
Note: all timestamps arguments should be `datetime` of any tz as they will be
converted to UTC ISO format strings
Args:
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): Filter results to a single action type.
before (Optional[datetime]): Latest cutoff for activity to include.
after (Optional[datetime]): Oldest cutoff for an activity to include.
cursor (Optional[datetime]): Timestamp cursor for pagination.
Returns:
Activity collection, filtered according to the provided parameters.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Activity"})
return super().activity(limit, action_type, before, after, cursor)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Fetches all of the current user's pending stream invitations.
Returns:
List[PendingStreamCollaborator]: A list of pending stream invitations.
"""
metrics.track(
metrics.SDK, self.account, {"name": "User Active Invites All Get"}
)
return super().get_all_pending_invites()
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Fetches a specific pending invite for the current user on a given stream.
Args:
stream_id (str): The ID of the stream to look for invites on.
token (Optional[str]): The token of the invite to look for (optional).
Returns:
Optional[PendingStreamCollaborator]: The invite for the given stream, or None if not found.
"""
metrics.track(metrics.SDK, self.account, {"name": "User Active Invite Get"})
return super().get_pending_invite(stream_id, token)
@@ -1,74 +0,0 @@
from typing import Optional
from specklepy.core.api.inputs.model_inputs import (
CreateModelInput,
DeleteModelInput,
ModelVersionsFilter,
UpdateModelInput,
)
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models import Model, ModelWithVersions, ResourceCollection
from specklepy.core.api.resources import ModelResource as CoreResource
from specklepy.logging import metrics
class ModelResource(CoreResource):
"""API Access class for models"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, model_id: str, project_id: str) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Get"})
return super().get(model_id, project_id)
def get_with_versions(
self,
model_id: str,
project_id: str,
*,
versions_limit: int = 25,
versions_cursor: Optional[str] = None,
versions_filter: Optional[ModelVersionsFilter] = None,
) -> ModelWithVersions:
metrics.track(metrics.SDK, self.account, {"name": "Model Get With Versions"})
return super().get_with_versions(
model_id,
project_id,
versions_limit=versions_limit,
versions_cursor=versions_cursor,
versions_filter=versions_filter,
)
def get_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ResourceCollection[Model]:
metrics.track(metrics.SDK, self.account, {"name": "Model Get Models"})
return super().get_models(
project_id,
models_limit=models_limit,
models_cursor=models_cursor,
models_filter=models_filter,
)
def create(self, input: CreateModelInput) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Create"})
return super().create(input)
def delete(self, input: DeleteModelInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Model Delete"})
return super().delete(input)
def update(self, input: UpdateModelInput) -> Model:
metrics.track(metrics.SDK, self.account, {"name": "Model Update"})
return super().update(input)
@@ -1,104 +0,0 @@
from datetime import datetime
from typing import List, Optional, Union
from deprecated import deprecated
from specklepy.core.api.models import (
ActivityCollection,
LimitedUser,
UserSearchResultCollection,
)
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources import OtherUserResource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class OtherUserResource(CoreResource):
"""
Provides API access to other users' profiles and activities on the platform.
This class enables fetching limited information about users, searching for users by name or email,
and accessing user activity logs with appropriate privacy and access control measures in place.
"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=(server_version,),
)
self.schema = LimitedUser
def get(self, id: str) -> Optional[LimitedUser]:
metrics.track(metrics.SDK, self.account, {"name": "Other User Get"})
return super().get(id)
def user_search(
self,
query: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
archived: bool = False,
emailOnly: bool = False,
) -> UserSearchResultCollection:
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().user_search(
query, limit=limit, cursor=cursor, archived=archived, emailOnly=emailOnly
)
@deprecated(reason="Use user_search instead", version=FE1_DEPRECATION_VERSION)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[LimitedUser], SpeckleException]:
"""
Searches for users by name or email.
The search requires a minimum query length of 3 characters.
Args:
search_query (str): The search string.
limit (int): Maximum number of search results to return.
Returns:
Union[List[LimitedUser], SpeckleException]: A list of users matching the search
query or an exception if the query is too short.
"""
if len(search_query) < 3:
return SpeckleException(
message="User search query must be at least 3 characters."
)
metrics.track(metrics.SDK, self.account, {"name": "Other User Search"})
return super().search(search_query, limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
user_id: str,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
) -> ActivityCollection:
"""
Retrieves a collection of activities for a specified user, with optional filters for activity type,
time frame, and pagination.
Args:
user_id (str): The ID of the user whose activities are being requested.
limit (int): The maximum number of activity items to return.
action_type (Optional[str]): A specific type of activity to filter.
before (Optional[datetime]): Latest timestamp to include activities before.
after (Optional[datetime]): Earliest timestamp to include activities after.
cursor (Optional[datetime]): Timestamp for pagination cursor.
Returns:
ActivityCollection: A collection of user activities filtered according to specified criteria.
"""
metrics.track(metrics.SDK, self.account, {"name": "Other User Activity"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@@ -1,54 +0,0 @@
from typing import Any, Optional, Tuple
from gql import Client
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import (
ProjectInviteCreateInput,
ProjectInviteUseInput,
)
from specklepy.core.api.models import PendingStreamCollaborator, ProjectWithTeam
from specklepy.core.api.resources import ProjectInviteResource as CoreResource
from specklepy.logging import metrics
class ProjectInviteResource(CoreResource):
"""API Access class for project invites"""
def __init__(
self,
account: Account,
basepath: str,
client: Client,
server_version: Optional[Tuple[Any, ...]],
) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def create(
self, project_id: str, input: ProjectInviteCreateInput
) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Create"})
return super().create(project_id, input)
def use(self, input: ProjectInviteUseInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Use"})
return super().use(input)
def get(
self, project_id: str, token: Optional[str]
) -> Optional[PendingStreamCollaborator]:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Get"})
return super().get(project_id, token)
def cancel(
self,
project_id: str,
invite_id: str,
) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Invite Cancel"})
return super().cancel(project_id, invite_id)
@@ -1,63 +0,0 @@
from typing import Optional
from specklepy.core.api.inputs.project_inputs import (
ProjectCreateInput,
ProjectModelsFilter,
ProjectUpdateInput,
ProjectUpdateRoleInput,
)
from specklepy.core.api.models import Project, ProjectWithModels, ProjectWithTeam
from specklepy.core.api.resources import ProjectResource as CoreResource
from specklepy.logging import metrics
class ProjectResource(CoreResource):
"""API Access class for projects"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, project_id: str) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Get "})
return super().get(project_id)
def get_with_models(
self,
project_id: str,
*,
models_limit: int = 25,
models_cursor: Optional[str] = None,
models_filter: Optional[ProjectModelsFilter] = None,
) -> ProjectWithModels:
metrics.track(metrics.SDK, self.account, {"name": "Project Get With Models"})
return super().get_with_models(
project_id,
models_limit=models_limit,
models_cursor=models_cursor,
models_filter=models_filter,
)
def get_with_team(self, project_id: str) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Get With Team"})
return super().get_with_team(project_id)
def create(self, input: ProjectCreateInput) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Create"})
return super().create(input)
def update(self, input: ProjectUpdateInput) -> Project:
metrics.track(metrics.SDK, self.account, {"name": "Project Update"})
return super().update(input)
def delete(self, project_id: str) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Project Delete"})
return super().delete(project_id)
def update_role(self, input: ProjectUpdateRoleInput) -> ProjectWithTeam:
metrics.track(metrics.SDK, self.account, {"name": "Project Update Role"})
return super().update_role(input)
@@ -1,70 +0,0 @@
from typing import Any, Dict, List, Tuple
from specklepy.api.models import ServerInfo
from specklepy.core.api.resources import ServerResource as CoreResource
from specklepy.logging import metrics
class ServerResource(CoreResource):
"""API Access class for the server"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
def get(self) -> ServerInfo:
"""Get the server info
Returns:
dict -- the server info in dictionary form
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Get"})
return super().get()
def version(self) -> Tuple[Any, ...]:
"""Get the server version
Returns:
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
return super().version()
def apps(self) -> Dict:
"""Get the apps registered on the server
Returns:
dict -- a dictionary of apps registered on the server
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Apps"})
return super().apps()
def create_token(self, name: str, scopes: List[str], lifespan: int) -> str:
"""Create a personal API token
Arguments:
scopes {List[str]} -- the scopes to grant with this token
name {str} -- a name for your new token
lifespan {int} -- duration before the token expires
Returns:
str -- the new API token. note: this is the only time you'll see the token!
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Create Token"})
return super().create_token(name, scopes, lifespan)
def revoke_token(self, token: str) -> bool:
"""Revokes (deletes) a personal API token
Arguments:
token {str} -- the token to revoke (delete)
Returns:
bool -- True if the token was successfully deleted
"""
metrics.track(metrics.SDK, self.account, {"name": "Server Revoke Token"})
return super().revoke_token(token)
@@ -1,64 +0,0 @@
from typing import Callable, Optional, Sequence
from pydantic import BaseModel
from typing_extensions import TypeVar
from specklepy.core.api.models import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
)
from specklepy.core.api.resources import SubscriptionResource as CoreResource
from specklepy.logging import metrics
TEventArgs = TypeVar("TEventArgs", bound=BaseModel)
class SubscriptionResource(CoreResource):
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
async def user_projects_updated(
self, callback: Callable[[UserProjectsUpdatedMessage], None]
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Models Updated"}
)
return await super().user_projects_updated(callback)
async def project_models_updated(
self,
callback: Callable[[ProjectModelsUpdatedMessage], None],
id: str,
*,
model_ids: Optional[Sequence[str]] = None,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Models Updated"}
)
return await super().project_models_updated(callback, id, model_ids=model_ids)
async def project_updated(
self,
callback: Callable[[ProjectUpdatedMessage], None],
id: str,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Updated"}
)
return await super().project_updated(callback, id)
async def project_versions_updated(
self,
callback: Callable[[ProjectVersionsUpdatedMessage], None],
id: str,
) -> None:
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Project Versions Updated"}
)
return await super().project_versions_updated(callback, id)
@@ -1,63 +0,0 @@
from typing import Optional
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.inputs.version_inputs import (
CreateVersionInput,
DeleteVersionsInput,
MarkReceivedVersionInput,
MoveVersionsInput,
UpdateVersionInput,
)
from specklepy.core.api.models import ResourceCollection, Version
from specklepy.core.api.resources import VersionResource as CoreResource
from specklepy.logging import metrics
class VersionResource(CoreResource):
"""API Access class for model versions"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
def get(self, version_id: str, project_id: str) -> Version:
metrics.track(metrics.SDK, self.account, {"name": "Version Get"})
return super().get(version_id, project_id)
def get_versions(
self,
model_id: str,
project_id: str,
*,
limit: int = 25,
cursor: Optional[str] = None,
filter: Optional[ModelVersionsFilter] = None,
) -> ResourceCollection[Version]:
metrics.track(metrics.SDK, self.account, {"name": "Version Get Versions"})
return super().get_versions(
model_id, project_id, limit=limit, cursor=cursor, filter=filter
)
def create(self, input: CreateVersionInput) -> str:
metrics.track(metrics.SDK, self.account, {"name": "Version Create"})
return super().create(input)
def update(self, input: UpdateVersionInput) -> Version:
metrics.track(metrics.SDK, self.account, {"name": "Version Update"})
return super().update(input)
def move_to_model(self, input: MoveVersionsInput) -> str:
metrics.track(metrics.SDK, self.account, {"name": "Version Move To Model"})
return super().move_to_model(input)
def delete(self, input: DeleteVersionsInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Version Delete"})
return super().delete(input)
def received(self, input: MarkReceivedVersionInput) -> bool:
metrics.track(metrics.SDK, self.account, {"name": "Version Received"})
return super().received(input)
@@ -1,9 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import ActiveUserResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(ActiveUserResource):
"""Renamed to ActiveUserResource"""
@@ -1,108 +0,0 @@
from typing import Optional, Union
from deprecated import deprecated
from specklepy.api.models import Branch
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.branch import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
"""API Access class for branches"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
self.schema = Branch
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self, stream_id: str, name: str, description: str = "No description provided"
) -> str:
"""Create a new branch on this stream
Arguments:
name {str} -- the name of the new branch
description {str} -- a short description of the branch
Returns:
id {str} -- the newly created branch's id
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Create"})
return super().create(stream_id, name, description)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(
self, stream_id: str, name: str, commits_limit: int = 10
) -> Union[Branch, None, SpeckleException]:
"""Get a branch by name from a stream
Arguments:
stream_id {str} -- the id of the stream to get the branch from
name {str} -- the name of the branch to get
commits_limit {int} -- maximum number of commits to get
Returns:
Branch -- the fetched branch with its latest commits
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Get"})
return super().get(stream_id, name, commits_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, branches_limit: int = 10, commits_limit: int = 10):
"""Get a list of branches from a given stream
Arguments:
stream_id {str} -- the id of the stream to get the branches from
branches_limit {int} -- maximum number of branches to get
commits_limit {int} -- maximum number of commits to get
Returns:
List[Branch] -- the branches on the stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch List"})
return super().list(stream_id, branches_limit, commits_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
stream_id: str,
branch_id: str,
name: Optional[str] = None,
description: Optional[str] = None,
):
"""Update a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to update
branch_id {str} -- the id of the branch to update
name {str} -- optional: the updated branch name
description {str} -- optional: the updated branch description
Returns:
bool -- True if update is successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Update"})
return super().update(stream_id, branch_id, name, description)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, branch_id: str):
"""Delete a branch
Arguments:
stream_id {str} -- the id of the stream containing the branch to delete
branch_id {str} -- the branch to delete
Returns:
bool -- True if deletion is successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Branch Delete"})
return super().delete(stream_id, branch_id)
@@ -1,134 +0,0 @@
from typing import List, Optional, Union
from deprecated import deprecated
from specklepy.api.models import Commit
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.commit import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
class Resource(CoreResource):
"""API Access class for commits"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
self.schema = Commit
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, commit_id: str) -> Commit:
"""
Gets a commit given a stream and the commit id
Arguments:
stream_id {str} -- the stream where we can find the commit
commit_id {str} -- the id of the commit you want to get
Returns:
Commit -- the retrieved commit object
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Get"})
return super().get(stream_id, commit_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_id: str, limit: int = 10) -> List[Commit]:
"""
Get a list of commits on a given stream
Arguments:
stream_id {str} -- the stream where the commits are
limit {int} -- the maximum number of commits to fetch (default = 10)
Returns:
List[Commit] -- a list of the most recent commit objects
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit List"})
return super().list(stream_id, limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
stream_id: str,
object_id: str,
branch_name: str = "main",
message: str = "",
source_application: str = "python",
parents: Optional[List[str]] = None,
) -> Union[str, SpeckleException]:
"""
Creates a commit on a branch
Arguments:
stream_id {str} -- the stream you want to commit to
object_id {str} -- the hash of your commit object
branch_name {str}
-- the name of the branch to commit to (defaults to "main")
message {str}
-- optional: a message to give more information about the commit
source_application{str}
-- optional: the application from which the commit was created
(defaults to "python")
parents {List[str]} -- optional: the id of the parent commits
Returns:
str -- the id of the created commit
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Create"})
return super().create(
stream_id, object_id, branch_name, message, source_application, parents
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(self, stream_id: str, commit_id: str, message: str) -> bool:
"""
Update a commit
Arguments:
stream_id {str}
-- the id of the stream that contains the commit you'd like to update
commit_id {str} -- the id of the commit you'd like to update
message {str} -- the updated commit message
Returns:
bool -- True if the operation succeeded
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Update"})
return super().update(stream_id, commit_id, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, stream_id: str, commit_id: str) -> bool:
"""
Delete a commit
Arguments:
stream_id {str}
-- the id of the stream that contains the commit you'd like to delete
commit_id {str} -- the id of the commit you'd like to delete
Returns:
bool -- True if the operation succeeded
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Delete"})
return super().delete(stream_id, commit_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def received(
self,
stream_id: str,
commit_id: str,
source_application: str = "python",
message: Optional[str] = None,
) -> bool:
"""
Mark a commit object a received by the source application.
"""
metrics.track(metrics.SDK, self.account, {"name": "Commit Received"})
return super().received(stream_id, commit_id, source_application, message)
@@ -1,63 +0,0 @@
from typing import Dict, List
from deprecated import deprecated
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.object import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.objects.base import Base
class Resource(CoreResource):
"""API Access class for objects"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
self.schema = Base
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, stream_id: str, object_id: str) -> Base:
"""
Get a stream object
Arguments:
stream_id {str} -- the id of the stream for the object
object_id {str} -- the hash of the object you want to get
Returns:
Base -- the returned Base object
"""
metrics.track(metrics.SDK, self.account, {"name": "Object Get"})
return super().get(stream_id, object_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(self, stream_id: str, objects: List[Dict]) -> str:
"""
Not advised - generally, you want to use `operations.send()`.
Create a new object on a stream.
To send a base object, you can prepare it by running it through the
`BaseObjectSerializer.traverse_base()` function to get a valid (serialisable)
object to send.
NOTE: this does not create a commit - you can create one with
`SpeckleClient.commit.create`.
Dynamic fields will be located in the 'data' dict of the received `Base` object
Arguments:
stream_id {str} -- the id of the stream you want to send the object to
objects {List[Dict]}
-- a list of base dictionary objects (NOTE: must be json serialisable)
Returns:
str -- the id of the object
"""
metrics.track(metrics.SDK, self.account, {"name": "Object Create"})
return super().create(stream_id, objects)
@@ -1,11 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import OtherUserResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to OtherUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(OtherUserResource):
"""
Renamed to OtherUserResource
"""
@@ -1,9 +0,0 @@
from deprecated import deprecated
from specklepy.api.resources import ServerResource
from specklepy.core.api.models.deprecated import FE1_DEPRECATION_VERSION
@deprecated(reason="Renamed to ActiveUserResource", version=FE1_DEPRECATION_VERSION)
class Resource(ServerResource):
"""Renamed to ServerResource"""
@@ -1,322 +0,0 @@
from datetime import datetime
from typing import List, Optional
from deprecated import deprecated
from specklepy.api.models import PendingStreamCollaborator, Stream
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.deprecated.stream import Resource as CoreResource
from specklepy.logging import metrics
class Resource(CoreResource):
"""API Access class for streams"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = Stream
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get(self, id: str, branch_limit: int = 10, commit_limit: int = 10) -> Stream:
"""Get the specified stream from the server
Arguments:
id {str} -- the stream id
branch_limit {int} -- the maximum number of branches to return
commit_limit {int} -- the maximum number of commits to return
Returns:
Stream -- the retrieved stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Get"})
return super().get(id, branch_limit, commit_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def list(self, stream_limit: int = 10) -> List[Stream]:
"""Get a list of the user's streams
Arguments:
stream_limit {int} -- The maximum number of streams to return
Returns:
List[Stream] -- A list of Stream objects
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream List"})
return super().list(stream_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def create(
self,
name: str = "Anonymous Python Stream",
description: str = "No description provided",
is_public: bool = True,
) -> str:
"""Create a new stream
Arguments:
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
id {str} -- the id of the newly created stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Create"})
return super().create(name, description, is_public)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update(
self,
id: str,
name: Optional[str] = None,
description: Optional[str] = None,
is_public: Optional[bool] = None,
) -> bool:
"""Update an existing stream
Arguments:
id {str} -- the id of the stream to be updated
name {str} -- the name of the string
description {str} -- a short description of the stream
is_public {bool}
-- whether or not the stream can be viewed by anyone with the id
Returns:
bool -- whether the stream update was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Update"})
return super().update(id, name, description, is_public)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def delete(self, id: str) -> bool:
"""Delete a stream given its id
Arguments:
id {str} -- the id of the stream to delete
Returns:
bool -- whether the deletion was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Delete"})
return super().delete(id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def search(
self,
search_query: str,
limit: int = 25,
branch_limit: int = 10,
commit_limit: int = 10,
):
"""Search for streams by name, description, or id
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
branch_limit {int} -- the maximum number of branches to return
commit_limit {int} -- the maximum number of commits to return
Returns:
List[Stream] -- a list of Streams that match the search query
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Search"})
return super().search(search_query, limit, branch_limit, commit_limit)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def favorite(self, stream_id: str, favorited: bool = True):
"""Favorite or unfavorite the given stream.
Arguments:
stream_id {str} -- the id of the stream to favorite / unfavorite
favorited {bool}
-- whether to favorite (True) or unfavorite (False) the stream
Returns:
Stream -- the stream with its `id`, `name`, and `favoritedDate`
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Favorite"})
return super().favorite(stream_id, favorited)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def get_all_pending_invites(
self, stream_id: str
) -> List[PendingStreamCollaborator]:
"""Get all of the pending invites on a stream.
You must be a `stream:owner` to query this.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the stream id from which to get the pending invites
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the specified stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Get"})
return super().get_all_pending_invites(stream_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite(
self,
stream_id: str,
email: Optional[str] = None,
user_id: Optional[str] = None,
role: str = "stream:contributor", # should default be reviewer?
message: Optional[str] = None,
):
"""Invite someone to a stream using either their email or user id
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
email {str} -- the email of the user to invite (use this OR `user_id`)
user_id {str} -- the id of the user to invite (use this OR `email`)
role {str}
-- the role to assign to the user (defaults to `stream:contributor`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Create"})
return super().invite(stream_id, email, user_id, role, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_batch(
self,
stream_id: str,
emails: Optional[List[str]] = None,
user_ids: Optional[List[None]] = None,
message: Optional[str] = None,
) -> bool:
"""Invite a batch of users to a specified stream.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to invite the user to
emails {List[str]}
-- the email of the user to invite (use this and/or `user_ids`)
user_id {List[str]}
-- the id of the user to invite (use this and/or `emails`)
message {str}
-- a message to send along with this invite to the specified user
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Batch Create"})
return super().invite_batch(stream_id, emails, user_ids, message)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_cancel(self, stream_id: str, invite_id: str) -> bool:
"""Cancel an existing stream invite
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream invite
invite_id {str} -- the id of the invite to use
Returns:
bool -- true if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Invite Cancel"})
return super().invite_cancel(stream_id, invite_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def invite_use(self, stream_id: str, token: str, accept: bool = True) -> bool:
"""Accept or decline a stream invite
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str}
-- the id of the stream for which the user has a pending invite
token {str} -- the token of the invite to use
accept {bool} -- whether or not to accept the invite (defaults to True)
Returns:
bool -- true if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Invite Use"})
return super().invite_use(stream_id, token, accept)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def update_permission(self, stream_id: str, user_id: str, role: str):
"""Updates permissions for a user on a given stream
Valid for Speckle Server >=2.6.4
Arguments:
stream_id {str} -- the id of the stream to grant permissions to
user_id {str} -- the id of the user to grant permissions for
role {str} -- the role to grant the user
Returns:
bool -- True if the operation was successful
"""
metrics.track(
metrics.SDK,
self.account,
{"name": "Stream Permission Update", "role": role},
)
return super().update_permission(stream_id, user_id, role)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def revoke_permission(self, stream_id: str, user_id: str):
"""Revoke permissions from a user on a given stream
Arguments:
stream_id {str} -- the id of the stream to revoke permissions from
user_id {str} -- the id of the user to revoke permissions from
Returns:
bool -- True if the operation was successful
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Permission Revoke"})
return super().revoke_permission(stream_id, user_id)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
def activity(
self,
stream_id: str,
action_type: Optional[str] = None,
limit: int = 20,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
Note: all timestamps arguments should be `datetime` of any tz
as they will be converted to UTC ISO format strings
stream_id {str} -- the id of the stream to get activity from
action_type {str}
-- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime}
-- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
metrics.track(metrics.SDK, self.account, {"name": "Stream Activity"})
return super().activity(stream_id, action_type, limit, before, after, cursor)
@@ -1,107 +0,0 @@
from typing import Callable, Dict, List, Optional, Union
from deprecated import deprecated
from graphql import DocumentNode
from specklepy.core.api.models.deprecated import (
FE1_DEPRECATION_REASON,
FE1_DEPRECATION_VERSION,
)
from specklepy.core.api.resources.current.subscription_resource import check_wsclient
from specklepy.core.api.resources.deprecated.subscriptions import (
Resource as CoreResource,
)
from specklepy.logging import metrics
class Resource(CoreResource):
"""API Access class for subscriptions"""
def __init__(self, account, basepath, client) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_added(self, callback: Optional[Callable] = None):
"""Subscribes to new stream added event for your profile.
Use this to display an up-to-date list of streams.
Arguments:
callback {Callable[Stream]} -- a function that takes the updated stream
as an argument and executes each time a stream is added
Returns:
Stream -- the update stream
"""
metrics.track(metrics.SDK, self.account, {"name": "Subscription Stream Added"})
return super().stream_added(callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_updated(self, id: str, callback: Optional[Callable] = None):
"""
Subscribes to stream updated event.
Use this in clients/components that pertain only to this stream.
Arguments:
id {str} -- the stream id of the stream to subscribe to
callback {Callable[Stream]}
-- a function that takes the updated stream
as an argument and executes each time the stream is updated
Returns:
Stream -- the update stream
"""
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Updated"}
)
return super().stream_updated(id, callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def stream_removed(self, callback: Optional[Callable] = None):
"""Subscribes to stream removed event for your profile.
Use this to display an up-to-date list of streams for your profile.
NOTE: If someone revokes your permissions on a stream,
this subscription will be triggered with an extra value of revokedBy
in the payload.
Arguments:
callback {Callable[Dict]}
-- a function that takes the returned dict as an argument
and executes each time a stream is removed
Returns:
dict -- dict containing 'id' of stream removed and optionally 'revokedBy'
"""
metrics.track(
metrics.SDK, self.account, {"name": "Subscription Stream Removed"}
)
return super().stream_removed(callback)
@deprecated(reason=FE1_DEPRECATION_REASON, version=FE1_DEPRECATION_VERSION)
@check_wsclient
async def subscribe(
self,
query: DocumentNode,
params: Optional[Dict] = None,
callback: Optional[Callable] = None,
return_type: Optional[Union[str, List]] = None,
schema=None,
parse_response: bool = True,
):
# if self.client.transport.websocket is None:
# TODO: add multiple subs to the same ws connection
async with self.client as session:
async for res in session.subscribe(query, variable_values=params):
res = self._step_into_response(response=res, return_type=return_type)
if parse_response:
res = self._parse_response(response=res, schema=schema)
if callback is not None:
callback(res)
else:
return res
@@ -1,153 +0,0 @@
from datetime import datetime
from typing import List, Optional, Union
from deprecated import deprecated
from specklepy.api.models import PendingStreamCollaborator, User
from specklepy.core.api.resources.deprecated.user import Resource as CoreResource
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
DEPRECATION_VERSION = "2.9.0"
DEPRECATION_TEXT = (
"The user resource is deprecated, please use the active_user or other_user"
" resources"
)
class Resource(CoreResource):
"""API Access class for users"""
def __init__(self, account, basepath, client, server_version) -> None:
super().__init__(
account=account,
basepath=basepath,
client=client,
server_version=server_version,
)
self.schema = User
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get(self, id: Optional[str] = None) -> User:
"""
Gets the profile of a user.
If no id argument is provided, will return the current authenticated
user's profile (as extracted from the authorization header).
Arguments:
id {str} -- the user id
Returns:
User -- the retrieved user
"""
metrics.track(metrics.SDK, self.account, {"name": "User Get_deprecated"})
return super().get(id)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def search(
self, search_query: str, limit: int = 25
) -> Union[List[User], SpeckleException]:
"""
Searches for user by name or email.
The search query must be at least 3 characters long
Arguments:
search_query {str} -- a string to search for
limit {int} -- the maximum number of results to return
Returns:
List[User] -- a list of User objects that match the search query
"""
metrics.track(metrics.SDK, self.account, {"name": "User Search_deprecated"})
return super().search(search_query, limit)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def update(
self,
name: Optional[str] = None,
company: Optional[str] = None,
bio: Optional[str] = None,
avatar: Optional[str] = None,
):
"""Updates your user profile. All arguments are optional.
Arguments:
name {str} -- your name
company {str} -- the company you may or may not work for
bio {str} -- tell us about yourself
avatar {str} -- a nice photo of yourself
Returns:
bool -- True if your profile was updated successfully
"""
# metrics.track(metrics.USER, self.account, {"name": "update"})
metrics.track(metrics.SDK, self.account, {"name": "User Update_deprecated"})
return super().update(name, company, bio, avatar)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def activity(
self,
user_id: Optional[str] = None,
limit: int = 20,
action_type: Optional[str] = None,
before: Optional[datetime] = None,
after: Optional[datetime] = None,
cursor: Optional[datetime] = None,
):
"""
Get the activity from a given stream in an Activity collection.
Step into the activity `items` for the list of activity.
If no id argument is provided, will return the current authenticated
user's activity (as extracted from the authorization header).
Note: all timestamps arguments should be `datetime` of any tz as
they will be converted to UTC ISO format strings
user_id {str} -- the id of the user to get the activity from
action_type {str} -- filter results to a single action type
(eg: `commit_create` or `commit_receive`)
limit {int} -- max number of Activity items to return
before {datetime}
-- latest cutoff for activity (ie: return all activity _before_ this time)
after {datetime}
-- oldest cutoff for activity (ie: return all activity _after_ this time)
cursor {datetime} -- timestamp cursor for pagination
"""
metrics.track(metrics.SDK, self.account, {"name": "User Activity_deprecated"})
return super().activity(user_id, limit, action_type, before, after, cursor)
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_all_pending_invites(self) -> List[PendingStreamCollaborator]:
"""Get all of the active user's pending stream invites
Requires Speckle Server version >= 2.6.4
Returns:
List[PendingStreamCollaborator]
-- a list of pending invites for the current user
"""
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(
metrics.SDK, self.account, {"name": "User GetAllInvites_deprecated"}
)
return super().get_all_pending_invites()
@deprecated(version=DEPRECATION_VERSION, reason=DEPRECATION_TEXT)
def get_pending_invite(
self, stream_id: str, token: Optional[str] = None
) -> Optional[PendingStreamCollaborator]:
"""Get a particular pending invite for the active user on a given stream.
If no invite_id is provided, any valid invite will be returned.
Requires Speckle Server version >= 2.6.4
Arguments:
stream_id {str} -- the id of the stream to look for invites on
token {str} -- the token of the invite to look for (optional)
Returns:
PendingStreamCollaborator
-- the invite for the given stream (or None if it isn't found)
"""
# metrics.track(metrics.INVITE, self.account, {"name": "get"})
metrics.track(metrics.SDK, self.account, {"name": "User GetInvite_deprecated"})
return super().get_pending_invite(stream_id, token)
-88
View File
@@ -1,88 +0,0 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import Account
from specklepy.core.api.wrapper import StreamWrapper as CoreStreamWrapper
from specklepy.logging import metrics
from specklepy.transports.server.server import ServerTransport
class StreamWrapper(CoreStreamWrapper):
"""
The `StreamWrapper` gives you some handy helpers to deal with urls and
get authenticated clients and transports.
Construct a `StreamWrapper` with a stream, branch, commit, or object URL.
The corresponding ids will be stored
in the wrapper. If you have local accounts on the machine,
you can use the `get_account` and `get_client` methods
to get a local account for the server. You can also pass a token into `get_client`
if you don't have a corresponding
local account for the server.
```py
from specklepy.api.wrapper import StreamWrapper
# provide any stream, branch, commit, object, or globals url
wrapper = StreamWrapper("https://app.speckle.systems/streams/3073b96e86/commits/604bea8cc6")
# get an authenticated SpeckleClient if you have a local account for the server
client = wrapper.get_client()
# get an authenticated ServerTransport if you have a local account for the server
transport = wrapper.get_transport()
```
"""
stream_url: str = None
use_ssl: bool = True
host: str = None
stream_id: str = None
commit_id: str = None
object_id: str = None
branch_name: str = None
_client: SpeckleClient = None
_account: Account = None
def __init__(self, url: str) -> None:
super().__init__(url=url)
def get_account(self, token: str = None) -> Account:
"""
Gets an account object for this server from the local accounts db
(added via Speckle Manager or a json file)
"""
metrics.track(metrics.SDK, custom_props={"name": "Stream Wrapper Get Account"})
return super().get_account(token)
def get_client(self, token: str = None) -> SpeckleClient:
"""
Gets an authenticated client for this server.
You may provide a token if there aren't any local accounts on this
machine. If no account is found and no token is provided,
an unauthenticated client is returned.
Arguments:
token {str}
-- optional token if no local account is available (defaults to None)
Returns:
SpeckleClient
-- authenticated with a corresponding local account or the provided token
"""
metrics.track(metrics.SDK, custom_props={"name": "Stream Wrapper Get Client"})
return super().get_client(token)
def get_transport(self, token: str = None) -> ServerTransport:
"""
Gets a server transport for this stream using an authenticated client.
If there is no local account for this
server and the client was not authenticated with a token,
this will throw an exception.
Returns:
ServerTransport -- constructed for this stream
with a pre-authenticated client
"""
metrics.track(
metrics.SDK, custom_props={"name": "Stream Wrapper Get Transport"}
)
return super().get_transport(token)
-6
View File
@@ -1,6 +0,0 @@
"""
This is the Core SDK module of `specklepy`.
This module should be kept in sync with the functionalities of our other SDKs especially
C# Core https://github.com/specklesystems/speckle-sharp/tree/main/Core/Core
"""
-29
View File
@@ -1,29 +0,0 @@
from enum import Enum
class ProjectVisibility(str, Enum):
PRIVATE = "PRIVATE"
PUBLIC = "PUBLIC"
UNLISTEd = "UNLISTED"
class UserProjectsUpdatedMessageType(str, Enum):
ADDED = "ADDED"
REMOVED = "REMOVED"
class ProjectModelsUpdatedMessageType(str, Enum):
CREATED = "CREATED"
DELETED = "DELETED"
UPDATED = "UPDATED"
class ProjectUpdatedMessageType(str, Enum):
DELETED = "DELETED"
UPDATED = "UPDATED"
class ProjectVersionsUpdatedMessageType(str, Enum):
CREATED = "CREATED"
DELETED = "DELETED"
UPDATED = "UPDATED"
-116
View File
@@ -1,116 +0,0 @@
from dataclasses import dataclass
from enum import Enum
from unicodedata import name
class HostAppVersion(Enum):
v = "v"
v6 = "v6"
v7 = "v7"
v2019 = "v2019"
v2020 = "v2020"
v2021 = "v2021"
v2022 = "v2022"
v2023 = "v2023"
v2024 = "v2024"
v2025 = "v2025"
vSandbox = "vSandbox"
vRevit = "vRevit"
vRevit2021 = "vRevit2021"
vRevit2022 = "vRevit2022"
vRevit2023 = "vRevit2023"
vRevit2024 = "vRevit2024"
vRevit2025 = "vRevit2025"
v25 = "v25"
v26 = "v26"
def __repr__(self) -> str:
return self.value
def __str__(self) -> str:
return self.value
@dataclass
class HostApplication:
name: str
slug: str
def get_version(self, version: HostAppVersion) -> str:
return f"{name.replace(' ', '')}{str(version).strip('v')}"
RHINO = HostApplication("Rhino", "rhino")
GRASSHOPPER = HostApplication("Grasshopper", "grasshopper")
REVIT = HostApplication("Revit", "revit")
DYNAMO = HostApplication("Dynamo", "dynamo")
UNITY = HostApplication("Unity", "unity")
GSA = HostApplication("GSA", "gsa")
CIVIL = HostApplication("Civil 3D", "civil3d")
AUTOCAD = HostApplication("AutoCAD", "autocad")
MICROSTATION = HostApplication("MicroStation", "microstation")
OPENROADS = HostApplication("OpenRoads", "openroads")
OPENRAIL = HostApplication("OpenRail", "openrail")
OPENBUILDINGS = HostApplication("OpenBuildings", "openbuildings")
ETABS = HostApplication("ETABS", "etabs")
SAP2000 = HostApplication("SAP2000", "sap2000")
CSIBRIDGE = HostApplication("CSIBridge", "csibridge")
SAFE = HostApplication("SAFE", "safe")
TEKLASTRUCTURES = HostApplication("Tekla Structures", "teklastructures")
DXF = HostApplication("DXF Converter", "dxf")
EXCEL = HostApplication("Excel", "excel")
UNREAL = HostApplication("Unreal", "unreal")
POWERBI = HostApplication("Power BI", "powerbi")
BLENDER = HostApplication("Blender", "blender")
QGIS = HostApplication("QGIS", "qgis")
ARCGIS = HostApplication("ArcGIS", "arcgis")
SKETCHUP = HostApplication("SketchUp", "sketchup")
ARCHICAD = HostApplication("Archicad", "archicad")
TOPSOLID = HostApplication("TopSolid", "topsolid")
PYTHON = HostApplication("Python", "python")
NET = HostApplication(".NET", "net")
OTHER = HostApplication("Other", "other")
_app_name_host_app_mapping = {
"dynamo": DYNAMO,
"revit": REVIT,
"autocad": AUTOCAD,
"civil": CIVIL,
"rhino": RHINO,
"grasshopper": GRASSHOPPER,
"unity": UNITY,
"gsa": GSA,
"microstation": MICROSTATION,
"openroads": OPENROADS,
"openrail": OPENRAIL,
"openbuildings": OPENBUILDINGS,
"etabs": ETABS,
"sap": SAP2000,
"csibridge": CSIBRIDGE,
"safe": SAFE,
"teklastructures": TEKLASTRUCTURES,
"dxf": DXF,
"excel": EXCEL,
"unreal": UNREAL,
"powerbi": POWERBI,
"blender": BLENDER,
"qgis": QGIS,
"arcgis": ARCGIS,
"sketchup": SKETCHUP,
"archicad": ARCHICAD,
"topsolid": TOPSOLID,
"python": PYTHON,
"net": NET,
}
def get_host_app_from_string(app_name: str) -> HostApplication:
app_name = app_name.lower().replace(" ", "")
for partial_app_name, host_app in _app_name_host_app_mapping.items():
if partial_app_name in app_name:
return host_app
return HostApplication(app_name, app_name)
if __name__ == "__main__":
print(HostAppVersion.v)
@@ -1,26 +0,0 @@
from typing import Optional, Sequence
from pydantic import BaseModel
class CreateModelInput(BaseModel):
name: str
description: Optional[str] = None
projectId: str
class DeleteModelInput(BaseModel):
id: str
projectId: str
class UpdateModelInput(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
projectId: str
class ModelVersionsFilter(BaseModel):
priorityIds: Sequence[str]
priorityIdsOnly: Optional[bool] = None
@@ -1,52 +0,0 @@
from typing import Optional, Sequence
from pydantic import BaseModel
from specklepy.core.api.enums import ProjectVisibility
class ProjectCreateInput(BaseModel):
name: Optional[str]
description: Optional[str]
visibility: Optional[ProjectVisibility]
class ProjectInviteCreateInput(BaseModel):
email: Optional[str]
role: Optional[str]
serverRole: Optional[str]
userId: Optional[str]
class ProjectInviteUseInput(BaseModel):
accept: bool
projectId: str
token: str
class ProjectModelsFilter(BaseModel):
contributors: Optional[Sequence[str]] = None
excludeIds: Optional[Sequence[str]] = None
ids: Optional[Sequence[str]] = None
onlyWithVersions: Optional[bool] = None
search: Optional[str] = None
sourceApps: Optional[Sequence[str]] = None
class ProjectUpdateInput(BaseModel):
id: str
name: Optional[str] = None
description: Optional[str] = None
allowPublicComments: Optional[bool] = None
visibility: Optional[ProjectVisibility] = None
class ProjectUpdateRoleInput(BaseModel):
userId: str
projectId: str
role: Optional[str]
class UserProjectsFilter(BaseModel):
search: str
onlyWithRole: Optional[Sequence[str]] = None
@@ -1,10 +0,0 @@
from typing import Optional
from pydantic import BaseModel
class UserUpdateInput(BaseModel):
avatar: Optional[str] = None
bio: Optional[str] = None
company: Optional[str] = None
name: Optional[str] = None
@@ -1,37 +0,0 @@
from typing import Optional, Sequence
from pydantic import BaseModel
class UpdateVersionInput(BaseModel):
versionId: str
projectId: str
message: Optional[str]
class MoveVersionsInput(BaseModel):
targetModelName: str
versionIds: Sequence[str]
projectId: str
class DeleteVersionsInput(BaseModel):
versionIds: Sequence[str]
projectId: str
class CreateVersionInput(BaseModel):
objectId: str
modelId: str
projectId: str
message: Optional[str] = None
sourceApplication: Optional[str] = "py"
totalChildrenCount: Optional[int] = None
parents: Optional[Sequence[str]] = None
class MarkReceivedVersionInput(BaseModel):
versionId: str
projectId: str
sourceApplication: str
message: Optional[str] = None
-77
View File
@@ -1,77 +0,0 @@
from specklepy.core.api.models.current import (
AuthStrategy,
LimitedUser,
Model,
ModelWithVersions,
PendingStreamCollaborator,
Project,
ProjectCollaborator,
ProjectCommentCollection,
ProjectWithModels,
ProjectWithTeam,
ResourceCollection,
ServerConfiguration,
ServerInfo,
ServerMigration,
User,
UserSearchResultCollection,
Version,
)
from specklepy.core.api.models.deprecated import (
Activity,
ActivityCollection,
Branch,
Branches,
Collaborator,
Commit,
Commits,
Object,
Stream,
Streams,
)
from specklepy.core.api.models.instances import InstanceDefinitionProxy, InstanceProxy
from specklepy.core.api.models.proxies import ColorProxy, GroupProxy
from specklepy.core.api.models.subscription_messages import (
ProjectModelsUpdatedMessage,
ProjectUpdatedMessage,
ProjectVersionsUpdatedMessage,
UserProjectsUpdatedMessage,
)
__all__ = [
"User",
"ResourceCollection",
"ServerMigration",
"AuthStrategy",
"ServerConfiguration",
"ServerInfo",
"LimitedUser",
"PendingStreamCollaborator",
"ProjectCollaborator",
"Version",
"Model",
"ModelWithVersions",
"Project",
"ProjectWithModels",
"ProjectWithTeam",
"ProjectCommentCollection",
"UserSearchResultCollection",
"UserProjectsUpdatedMessage",
"ProjectModelsUpdatedMessage",
"ProjectUpdatedMessage",
"ProjectVersionsUpdatedMessage",
"Collaborator",
"Commit",
"Commits",
"Object",
"Branch",
"Branches",
"Stream",
"Streams",
"Activity",
"ActivityCollection",
"InstanceProxy",
"InstanceDefinitionProxy",
"ColorProxy",
"GroupProxy",
]
-171
View File
@@ -1,171 +0,0 @@
from datetime import datetime
from typing import Generic, List, Optional, TypeVar
from pydantic import BaseModel
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.models.deprecated import Streams
T = TypeVar("T")
class User(BaseModel):
id: str
email: Optional[str] = None
name: str
bio: Optional[str] = None
company: Optional[str] = None
avatar: Optional[str] = None
verified: Optional[bool] = None
role: Optional[str] = None
streams: Optional["Streams"] = None
def __repr__(self):
return (
f"User( id: {self.id}, name: {self.name}, email: {self.email}, company:"
f" {self.company} )"
)
def __str__(self) -> str:
return self.__repr__()
class ResourceCollection(BaseModel, Generic[T]):
totalCount: int
items: List[T]
cursor: Optional[str] = None
class ServerMigration(BaseModel):
movedFrom: Optional[str]
movedTo: Optional[str]
class AuthStrategy(BaseModel):
color: Optional[str]
icon: str
id: str
name: str
url: str
class ServerConfiguration(BaseModel):
blobSizeLimitBytes: int
objectMultipartUploadSizeLimitBytes: int
objectSizeLimitBytes: int
# Keeping this one all Optionals at the minute, because its used both as a deserialization model for GQL and Account Management
class ServerInfo(BaseModel):
name: Optional[str] = None
company: Optional[str] = None
url: Optional[str] = None
adminContact: Optional[str] = None
description: Optional[str] = None
canonicalUrl: Optional[str] = None
roles: Optional[List[dict]] = None
scopes: Optional[List[dict]] = None
authStrategies: Optional[List[dict]] = None
version: Optional[str] = None
frontend2: Optional[bool] = None
migration: Optional[ServerMigration] = None
# TODO separate gql model from account management model
class LimitedUser(BaseModel):
"""Limited user type, for showing public info about a user to another user."""
id: str
name: str
bio: Optional[str]
company: Optional[str]
avatar: Optional[str]
verified: Optional[bool]
role: Optional[str]
class PendingStreamCollaborator(BaseModel):
id: str
inviteId: str
streamId: Optional[str] = None
projectId: str
streamName: Optional[str] = None
projectName: str
title: str
role: str
invitedBy: LimitedUser
user: Optional[LimitedUser] = None
token: Optional[str]
def __repr__(self):
return (
f"PendingStreamCollaborator( inviteId: {self.inviteId}, streamId:"
f" {self.streamId}, role: {self.role}, title: {self.title}, invitedBy:"
f" {self.user.name if self.user else None})"
)
def __str__(self) -> str:
return self.__repr__()
class ProjectCollaborator(BaseModel):
id: str
role: str
user: LimitedUser
class Version(BaseModel):
authorUser: Optional[LimitedUser]
createdAt: datetime
id: str
message: Optional[str]
previewUrl: str
referencedObject: str
sourceApplication: Optional[str]
class Model(BaseModel):
author: LimitedUser
createdAt: datetime
description: Optional[str]
displayName: str
id: str
name: str
previewUrl: Optional[str]
updatedAt: datetime
class ModelWithVersions(Model):
versions: ResourceCollection[Version]
class Project(BaseModel):
allowPublicComments: bool
createdAt: datetime
description: Optional[str]
id: str
name: str
role: Optional[str]
sourceApps: List[str]
updatedAt: datetime
visibility: ProjectVisibility
workspaceId: Optional[str]
class ProjectWithModels(Project):
models: ResourceCollection[Model]
class ProjectWithTeam(Project):
invitedTeam: List[PendingStreamCollaborator]
team: List[ProjectCollaborator]
class ProjectCommentCollection(ResourceCollection[T], Generic[T]):
totalArchivedCount: int
class UserSearchResultCollection(BaseModel):
items: List[LimitedUser]
cursor: Optional[str] = None

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