Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 66eab146d6 | |||
| aab424bbff | |||
| 2f2e8ba734 | |||
| 9685a2741b | |||
| 5702d116d0 | |||
| d440bb5c0f | |||
| 309c78da37 | |||
| ff812d5ad9 | |||
| 8edc0d5d78 | |||
| 78b3e99475 | |||
| ac9e081d49 | |||
| 4bc95441b9 | |||
| 0d74848b68 | |||
| 8a76006f9e | |||
| af42b09dd5 | |||
| e4453f0b04 | |||
| c9a0e45171 | |||
| f20fc7edb3 | |||
| 0cd0c3a1f6 | |||
| 2594ce0382 | |||
| ec67f5ba48 | |||
| db61d2e99c | |||
| 69090f6eb1 |
@@ -0,0 +1,43 @@
|
||||
name: docs
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v3.*.*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Configure Git Credentials
|
||||
run: |
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Cache dependencies
|
||||
run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
key: mkdocs-material-${{ env.cache_id }}
|
||||
path: ~/.cache
|
||||
restore-keys: |
|
||||
mkdocs-material-
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Install docs dependencies
|
||||
run: uv sync --group docs
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
run: uv run mkdocs gh-deploy --force
|
||||
+50
-10
@@ -9,8 +9,55 @@ on:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
test-internal: # Run integration tests against the internal server image
|
||||
name: Test (internal)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.10"
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv and set the python version
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Run Speckle Server
|
||||
run: docker compose --file docker-compose-internal.yml up --detach --wait
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
|
||||
|
||||
- uses: codecov/codecov-action@v5
|
||||
if: matrix.python-version == 3.12
|
||||
with:
|
||||
fail_ci_if_error: true # optional (default = false)
|
||||
files: ./reports/test-results.xml # optional
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
|
||||
test-public: # Run integration tests against the public server image
|
||||
name: Test (public)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -42,17 +89,10 @@ jobs:
|
||||
run: uv run pre-commit run --all-files
|
||||
|
||||
- name: Run Speckle Server
|
||||
run: docker compose up --detach --wait
|
||||
run: docker compose --file docker-compose.yml up --detach --wait
|
||||
|
||||
- name: Run tests
|
||||
run: uv run pytest --cov --cov-report xml:reports/coverage.xml --junitxml=reports/test-results.xml
|
||||
|
||||
- uses: codecov/codecov-action@v5
|
||||
if: matrix.python-version == 3.12
|
||||
with:
|
||||
fail_ci_if_error: true # optional (default = false)
|
||||
files: ./reports/test-results.xml # optional
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Minimize uv cache
|
||||
run: uv cache prune --ci
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
name: "speckle-server"
|
||||
|
||||
services:
|
||||
####
|
||||
# Speckle Server dependencies
|
||||
#######
|
||||
postgres:
|
||||
image: "postgres:16.4-alpine3.20@sha256:d898b0b78a2627cb4ee63464a14efc9d296884f1b28c841b0ab7d7c42f1fffdf"
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_DB: speckle
|
||||
POSTGRES_USER: speckle
|
||||
POSTGRES_PASSWORD: speckle
|
||||
volumes:
|
||||
- ./.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: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.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:
|
||||
- ./.volumes/minio-data:/data
|
||||
ports:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '127.0.0.1:9001:9001'
|
||||
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:
|
||||
image: ghcr.io/specklesystems/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(Number(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"
|
||||
FRONTEND_ORIGIN: "http://127.0.0.1:8081"
|
||||
|
||||
# TODO: Change thvolumes:
|
||||
REDIS_URL: "redis://redis"
|
||||
|
||||
S3_ENDPOINT: "http://minio:9000"
|
||||
S3_PUBLIC_ENDPOINT: "http://127.0.0.1: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"
|
||||
|
||||
POSTGRES_URL: "postgres"
|
||||
POSTGRES_USER: "speckle"
|
||||
POSTGRES_PASSWORD: "speckle"
|
||||
POSTGRES_DB: "speckle"
|
||||
ENABLE_MP: "false"
|
||||
|
||||
LOG_PRETTY: "true"
|
||||
|
||||
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
|
||||
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: speckle-server
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
minio-data:
|
||||
+6
-6
@@ -1,4 +1,3 @@
|
||||
version: "3.9"
|
||||
name: "speckle-server"
|
||||
|
||||
services:
|
||||
@@ -22,7 +21,7 @@ services:
|
||||
retries: 30
|
||||
|
||||
redis:
|
||||
image: "redis:6.0-alpine"
|
||||
image: "valkey/valkey:8.1-alpine@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4"
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.volumes/redis-data:/data
|
||||
@@ -38,6 +37,9 @@ services:
|
||||
restart: always
|
||||
volumes:
|
||||
- ./.volumes/minio-data:/data
|
||||
ports:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '127.0.0.1:9001:9001'
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
@@ -48,8 +50,6 @@ services:
|
||||
timeout: 30s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
ports:
|
||||
- "0.0.0.0:9000:9000"
|
||||
|
||||
speckle-server:
|
||||
image: speckle/speckle-server:latest
|
||||
@@ -96,7 +96,6 @@ services:
|
||||
SESSION_SECRET: "TODO:ReplaceWithLongString"
|
||||
|
||||
STRATEGY_LOCAL: "true"
|
||||
DEBUG: "speckle:*"
|
||||
|
||||
POSTGRES_URL: "postgres"
|
||||
POSTGRES_USER: "speckle"
|
||||
@@ -104,9 +103,10 @@ services:
|
||||
POSTGRES_DB: "speckle"
|
||||
ENABLE_MP: "false"
|
||||
|
||||
LOG_PRETTY: "true"
|
||||
|
||||
FF_NEXT_GEN_FILE_IMPORTER_ENABLED: "true"
|
||||
FF_LARGE_FILE_IMPORTS_ENABLED: "true"
|
||||
FF_BACKGROUND_JOBS_ENABLED: "true"
|
||||
|
||||
networks:
|
||||
default:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 386 B |
@@ -0,0 +1,50 @@
|
||||
# specklepy API Reference
|
||||
|
||||
> The Python SDK for Speckle - Build powerful AEC data workflows
|
||||
|
||||
**specklepy** is the Python SDK for Speckle, enabling you to interact with Speckle Server, send and receive geometry, and build custom integrations for the AEC industry.
|
||||
|
||||
## What is specklepy?
|
||||
|
||||
specklepy is a comprehensive Python library that provides:
|
||||
|
||||
* **Object-based data exchange** - Send and receive geometry and BIM data without files
|
||||
* **GraphQL API client** - Full access to Speckle Server's API
|
||||
* **Extensible object model** - Create custom objects that inherit from `Base`
|
||||
* **Multiple transport options** - Store data locally (SQLite), in-memory, or on Speckle Server
|
||||
* **Geometry support** - Rich geometric primitives (Point, Line, Mesh, etc.)
|
||||
|
||||
## Speckle Automate
|
||||
|
||||
Speckle Automate is a fully fledged CI/CD platform designed to run custom code on Speckle models whenever a new version is available.
|
||||
|
||||
As a software developer, you can develop Functions that others in your team consume in Automations. From creating reports to running code compliance checks to wind simulations, there is no limit to what you can do with Automate.
|
||||
|
||||
## Installation
|
||||
|
||||
Install specklepy using pip:
|
||||
|
||||
```bash
|
||||
pip install specklepy
|
||||
```
|
||||
|
||||
## Quick Example
|
||||
|
||||
```python
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.credentials import get_default_account
|
||||
from specklepy.objects.geometry import Point
|
||||
|
||||
# Authenticate
|
||||
client = SpeckleClient(host="https://app.speckle.systems")
|
||||
account = get_default_account()
|
||||
client.authenticate_with_account(account)
|
||||
|
||||
# Create geometry
|
||||
point = Point(x=10, y=20, z=5)
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Community Forum**: [speckle.community](https://speckle.community/c/help/developers)
|
||||
- **GitHub Issues**: [github.com/specklesystems/specklepy](https://github.com/specklesystems/specklepy/issues)
|
||||
@@ -0,0 +1 @@
|
||||
::: speckle_automate.AutomationContext
|
||||
@@ -0,0 +1,3 @@
|
||||
::: speckle_automate.runner.execute_automate_function
|
||||
|
||||
::: speckle_automate.runner.run_function
|
||||
@@ -0,0 +1,11 @@
|
||||
::: speckle_automate.AutomateBase
|
||||
|
||||
::: speckle_automate.AutomationRunData
|
||||
|
||||
::: speckle_automate.AutomationResult
|
||||
|
||||
::: speckle_automate.ResultCase
|
||||
|
||||
::: speckle_automate.AutomationStatus
|
||||
|
||||
::: speckle_automate.ObjectResultLevel
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.client.SpeckleClient
|
||||
@@ -0,0 +1,5 @@
|
||||
::: specklepy.api.credentials.Account
|
||||
|
||||
::: specklepy.api.credentials.UserInfo
|
||||
|
||||
::: specklepy.api.credentials.StreamWrapper
|
||||
@@ -0,0 +1,7 @@
|
||||
::: specklepy.api.operations.send
|
||||
|
||||
::: specklepy.api.operations.receive
|
||||
|
||||
::: specklepy.api.operations.serialize
|
||||
|
||||
::: specklepy.api.operations.deserialize
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.ActiveUserResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.FileImportResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.ModelResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.OtherUserResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.ProjectInviteResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.ProjectResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.ServerResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.SubscriptionResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.VersionResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.api.resources.WorkspaceResource
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.core.api.enums.ProjectVisibility
|
||||
@@ -0,0 +1,11 @@
|
||||
::: specklepy.core.api.inputs.ProjectCreateInput
|
||||
|
||||
::: specklepy.core.api.inputs.ProjectUpdateInput
|
||||
|
||||
::: specklepy.core.api.inputs.CreateModelInput
|
||||
|
||||
::: specklepy.core.api.inputs.UpdateModelInput
|
||||
|
||||
::: specklepy.core.api.inputs.CreateVersionInput
|
||||
|
||||
::: specklepy.core.api.inputs.UpdateVersionInput
|
||||
@@ -0,0 +1,13 @@
|
||||
::: specklepy.core.api.models.User
|
||||
|
||||
::: specklepy.core.api.models.LimitedUser
|
||||
|
||||
::: specklepy.core.api.models.ServerInfo
|
||||
|
||||
::: specklepy.core.api.models.Project
|
||||
|
||||
::: specklepy.core.api.models.Model
|
||||
|
||||
::: specklepy.core.api.models.Version
|
||||
|
||||
::: specklepy.core.api.models.current.Workspace
|
||||
@@ -0,0 +1,7 @@
|
||||
::: specklepy.logging.exceptions.SpeckleException
|
||||
|
||||
::: specklepy.logging.exceptions.GraphQLException
|
||||
|
||||
::: specklepy.logging.exceptions.SerializationException
|
||||
|
||||
::: specklepy.logging.exceptions.SpeckleWarning
|
||||
@@ -0,0 +1,3 @@
|
||||
::: specklepy.objects.Base
|
||||
|
||||
::: specklepy.objects.base.DataChunk
|
||||
@@ -0,0 +1,5 @@
|
||||
::: specklepy.objects.DataObject
|
||||
|
||||
::: specklepy.objects.QgisObject
|
||||
|
||||
::: specklepy.objects.BlenderObject
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Arc
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Box
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Circle
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.ControlPoint
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Curve
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Ellipse
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Line
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Mesh
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Plane
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Point
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.PointCloud
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Polycurve
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Polyline
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Spiral
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Surface
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.geometry.Vector
|
||||
@@ -0,0 +1,7 @@
|
||||
::: specklepy.objects.graph_traversal.GraphTraversal
|
||||
|
||||
::: specklepy.objects.graph_traversal.TraversalContext
|
||||
|
||||
::: specklepy.objects.graph_traversal.TraversalRule
|
||||
|
||||
::: specklepy.objects.graph_traversal.DefaultRule
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.models.collections.collection.Collection
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.other.RenderMaterial
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.primitive.Interval
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.proxies.ColorProxy
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.proxies.GroupProxy
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.proxies.InstanceDefinitionProxy
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.proxies.InstanceProxy
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.proxies.LevelProxy
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.objects.proxies.RenderMaterialProxy
|
||||
@@ -0,0 +1 @@
|
||||
::: specklepy.serialization.base_object_serializer.BaseObjectSerializer
|
||||
@@ -0,0 +1,7 @@
|
||||
::: specklepy.transports.abstract_transport.AbstractTransport
|
||||
|
||||
::: specklepy.transports.memory.MemoryTransport
|
||||
|
||||
::: specklepy.transports.sqlite.SQLiteTransport
|
||||
|
||||
::: specklepy.transports.server.ServerTransport
|
||||
@@ -0,0 +1,304 @@
|
||||
.md-content h1 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.md-content h1 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-content h1 {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.md-content h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
letter-spacing: -0.025em;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.md-content h2 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-content h2 {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.md-content h3 {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.md-content h3 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-content h3 {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.md-content p,
|
||||
.md-content li {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.6;
|
||||
color: #374151;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-content p,
|
||||
[data-md-color-scheme="slate"] .md-content li {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.doc.doc-object-name {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .doc.doc-object-name {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.doc.doc-heading {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .doc.doc-heading {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.md-content code {
|
||||
font-size: 0.75rem;
|
||||
color: #dc2626;
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-content code {
|
||||
color: #fca5a5;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.md-content pre code {
|
||||
font-size: 0.75rem;
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.md-content a {
|
||||
color: #2563eb;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.md-content a:hover {
|
||||
color: #1d4ed8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-content a {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-content a:hover {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] {
|
||||
--md-default-bg-color: #000000;
|
||||
--md-default-fg-color: #ffffff;
|
||||
--md-code-bg-color: #0a0a0a;
|
||||
--md-code-fg-color: #ffffff;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-header {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-tabs {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-footer {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-sidebar {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .md-nav {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.highlight pre,
|
||||
.highlight code {
|
||||
background-color: #f6f8fa !important;
|
||||
color: #24292e !important;
|
||||
}
|
||||
|
||||
.highlight .k,
|
||||
.highlight .kc,
|
||||
.highlight .kd,
|
||||
.highlight .kn,
|
||||
.highlight .kp,
|
||||
.highlight .kr,
|
||||
.highlight .kt {
|
||||
color: #d73a49 !important;
|
||||
}
|
||||
|
||||
.highlight .s,
|
||||
.highlight .s1,
|
||||
.highlight .s2,
|
||||
.highlight .sb,
|
||||
.highlight .sc,
|
||||
.highlight .sd,
|
||||
.highlight .se,
|
||||
.highlight .sh,
|
||||
.highlight .si,
|
||||
.highlight .sx,
|
||||
.highlight .sr,
|
||||
.highlight .ss {
|
||||
color: #032f62 !important;
|
||||
}
|
||||
|
||||
.highlight .nf,
|
||||
.highlight .fm,
|
||||
.highlight .nc {
|
||||
color: #6f42c1 !important;
|
||||
}
|
||||
|
||||
.highlight .m,
|
||||
.highlight .mf,
|
||||
.highlight .mh,
|
||||
.highlight .mi,
|
||||
.highlight .mo {
|
||||
color: #005cc5 !important;
|
||||
}
|
||||
|
||||
.highlight .c,
|
||||
.highlight .c1,
|
||||
.highlight .cm,
|
||||
.highlight .cp,
|
||||
.highlight .cs {
|
||||
color: #6a737d !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.highlight .o,
|
||||
.highlight .ow {
|
||||
color: #d73a49 !important;
|
||||
}
|
||||
|
||||
.highlight .n,
|
||||
.highlight .na,
|
||||
.highlight .nb,
|
||||
.highlight .nd,
|
||||
.highlight .ni,
|
||||
.highlight .nl,
|
||||
.highlight .nn,
|
||||
.highlight .nx,
|
||||
.highlight .py,
|
||||
.highlight .nt,
|
||||
.highlight .nv,
|
||||
.highlight .bp,
|
||||
.highlight .vc,
|
||||
.highlight .vg,
|
||||
.highlight .vi {
|
||||
color: #e36209 !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .highlight pre,
|
||||
[data-md-color-scheme="slate"] .highlight code {
|
||||
background-color: #0d1117 !important;
|
||||
color: #e6edf3 !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .highlight .k,
|
||||
[data-md-color-scheme="slate"] .highlight .kc,
|
||||
[data-md-color-scheme="slate"] .highlight .kd,
|
||||
[data-md-color-scheme="slate"] .highlight .kn,
|
||||
[data-md-color-scheme="slate"] .highlight .kp,
|
||||
[data-md-color-scheme="slate"] .highlight .kr,
|
||||
[data-md-color-scheme="slate"] .highlight .kt {
|
||||
color: #ff7b72 !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .highlight .s,
|
||||
[data-md-color-scheme="slate"] .highlight .s1,
|
||||
[data-md-color-scheme="slate"] .highlight .s2,
|
||||
[data-md-color-scheme="slate"] .highlight .sb,
|
||||
[data-md-color-scheme="slate"] .highlight .sc,
|
||||
[data-md-color-scheme="slate"] .highlight .sd,
|
||||
[data-md-color-scheme="slate"] .highlight .se,
|
||||
[data-md-color-scheme="slate"] .highlight .sh,
|
||||
[data-md-color-scheme="slate"] .highlight .si,
|
||||
[data-md-color-scheme="slate"] .highlight .sx,
|
||||
[data-md-color-scheme="slate"] .highlight .sr,
|
||||
[data-md-color-scheme="slate"] .highlight .ss {
|
||||
color: #a5d6ff !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .highlight .nf,
|
||||
[data-md-color-scheme="slate"] .highlight .fm,
|
||||
[data-md-color-scheme="slate"] .highlight .nc {
|
||||
color: #d2a8ff !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .highlight .m,
|
||||
[data-md-color-scheme="slate"] .highlight .mf,
|
||||
[data-md-color-scheme="slate"] .highlight .mh,
|
||||
[data-md-color-scheme="slate"] .highlight .mi,
|
||||
[data-md-color-scheme="slate"] .highlight .mo {
|
||||
color: #79c0ff !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .highlight .c,
|
||||
[data-md-color-scheme="slate"] .highlight .c1,
|
||||
[data-md-color-scheme="slate"] .highlight .cm,
|
||||
[data-md-color-scheme="slate"] .highlight .cp,
|
||||
[data-md-color-scheme="slate"] .highlight .cs {
|
||||
color: #8b949e !important;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .highlight .o,
|
||||
[data-md-color-scheme="slate"] .highlight .ow {
|
||||
color: #ff7b72 !important;
|
||||
}
|
||||
|
||||
[data-md-color-scheme="slate"] .highlight .n,
|
||||
[data-md-color-scheme="slate"] .highlight .na,
|
||||
[data-md-color-scheme="slate"] .highlight .nb,
|
||||
[data-md-color-scheme="slate"] .highlight .nd,
|
||||
[data-md-color-scheme="slate"] .highlight .ni,
|
||||
[data-md-color-scheme="slate"] .highlight .nl,
|
||||
[data-md-color-scheme="slate"] .highlight .nn,
|
||||
[data-md-color-scheme="slate"] .highlight .nx,
|
||||
[data-md-color-scheme="slate"] .highlight .py,
|
||||
[data-md-color-scheme="slate"] .highlight .nt,
|
||||
[data-md-color-scheme="slate"] .highlight .nv,
|
||||
[data-md-color-scheme="slate"] .highlight .bp,
|
||||
[data-md-color-scheme="slate"] .highlight .vc,
|
||||
[data-md-color-scheme="slate"] .highlight .vg,
|
||||
[data-md-color-scheme="slate"] .highlight .vi {
|
||||
color: #ffa657 !important;
|
||||
}
|
||||
@@ -1,6 +1,20 @@
|
||||
[tools]
|
||||
python = "3.13.7"
|
||||
uv = "0.9.11"
|
||||
|
||||
[settings]
|
||||
experimental = true
|
||||
python.uv_venv_auto = true
|
||||
|
||||
|
||||
[tasks.install]
|
||||
run= "uv sync --all-groups"
|
||||
|
||||
|
||||
[tasks.install_docs]
|
||||
run= "uv sync --group docs"
|
||||
|
||||
[tasks.build_docs]
|
||||
description = "Build static docs "
|
||||
run = "uv run mkdocs build"
|
||||
depends = ['install_docs']
|
||||
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
site_name: specklepy Docs
|
||||
theme:
|
||||
name: material
|
||||
font:
|
||||
text: Inter
|
||||
favicon: assets/speckle_logo.png
|
||||
logo: assets/speckle_logo.png
|
||||
features:
|
||||
- navigation.tabs
|
||||
palette:
|
||||
# Palette toggle for light mode
|
||||
- scheme: default
|
||||
primary: white
|
||||
toggle:
|
||||
icon: material/weather-night
|
||||
name: Switch to dark mode
|
||||
|
||||
# Palette toggle for dark mode
|
||||
- scheme: slate
|
||||
primary: black
|
||||
logo: assets/logo_white.png
|
||||
toggle:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
|
||||
|
||||
|
||||
markdown_extensions:
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
line_spans: __span
|
||||
pygments_lang_class: true
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.snippets
|
||||
- pymdownx.superfences
|
||||
|
||||
extra_css:
|
||||
- stylesheets/extra.css
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths: [src]
|
||||
options:
|
||||
parameter_headings: false
|
||||
members_order: source
|
||||
separate_signature: true
|
||||
filters: ["!^_"] #Ignore _ prefixed properties
|
||||
docstring_options:
|
||||
ignore_init_summary: true
|
||||
merge_init_into_class: true
|
||||
show_signature_annotations: true
|
||||
signature_crossrefs: true
|
||||
show_if_no_docstring: true
|
||||
show_labels: true
|
||||
show_source: true
|
||||
show_symbol_type_heading: true
|
||||
show_symbol_type_toc: true
|
||||
show_bases: false
|
||||
heading_level: 3
|
||||
|
||||
inventories:
|
||||
- url: https://docs.python.org/3/objects.inv
|
||||
domains: [py, std]
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- specklepy SDK:
|
||||
- API:
|
||||
- Client: specklepy/api/client.md
|
||||
- Credentials: specklepy/api/credentials.md
|
||||
- Operations: specklepy/api/operations.md
|
||||
- Resources:
|
||||
- ActiveUserResource: specklepy/api/resources/ActiveUserResource.md
|
||||
- FileImportResource: specklepy/api/resources/FileImportResource.md
|
||||
- ModelResource: specklepy/api/resources/ModelResource.md
|
||||
- OtherUserResource: specklepy/api/resources/OtherUserResource.md
|
||||
- ProjectInviteResource: specklepy/api/resources/ProjectInviteResource.md
|
||||
- ProjectResource: specklepy/api/resources/ProjectResource.md
|
||||
- ServerResource: specklepy/api/resources/ServerResource.md
|
||||
- SubscriptionResource: specklepy/api/resources/SubscriptionResource.md
|
||||
- VersionResource: specklepy/api/resources/VersionResource.md
|
||||
- WorkspaceResource: specklepy/api/resources/WorkspaceResource.md
|
||||
- Objects:
|
||||
- Base: specklepy/objects/base.md
|
||||
- Data Objects: specklepy/objects/data_objects.md
|
||||
- Geometry:
|
||||
- Arc: specklepy/objects/geometry/Arc.md
|
||||
- Box: specklepy/objects/geometry/Box.md
|
||||
- Circle: specklepy/objects/geometry/Circle.md
|
||||
- ControlPoint: specklepy/objects/geometry/ControlPoint.md
|
||||
- Curve: specklepy/objects/geometry/Curve.md
|
||||
- Ellipse: specklepy/objects/geometry/Ellipse.md
|
||||
- Line: specklepy/objects/geometry/Line.md
|
||||
- Mesh: specklepy/objects/geometry/Mesh.md
|
||||
- Plane: specklepy/objects/geometry/Plane.md
|
||||
- Point: specklepy/objects/geometry/Point.md
|
||||
- PointCloud: specklepy/objects/geometry/PointCloud.md
|
||||
- Polycurve: specklepy/objects/geometry/Polycurve.md
|
||||
- Polyline: specklepy/objects/geometry/Polyline.md
|
||||
- Spiral: specklepy/objects/geometry/Spiral.md
|
||||
- Surface: specklepy/objects/geometry/Surface.md
|
||||
- Vector: specklepy/objects/geometry/Vector.md
|
||||
- Primitives:
|
||||
- Interval: specklepy/objects/primitives/interval.md
|
||||
- Other:
|
||||
- RenderMaterial: specklepy/objects/other/render_material.md
|
||||
- Collection: specklepy/objects/other/collection.md
|
||||
- Proxies:
|
||||
- ColorProxy: specklepy/objects/proxies/ColorProxy.md
|
||||
- GroupProxy: specklepy/objects/proxies/GroupProxy.md
|
||||
- InstanceProxy: specklepy/objects/proxies/InstanceProxy.md
|
||||
- InstanceDefinitionProxy: specklepy/objects/proxies/InstanceDefinitionProxy.md
|
||||
- LevelProxy: specklepy/objects/proxies/LevelProxy.md
|
||||
- RenderMaterialProxy: specklepy/objects/proxies/RenderMaterialProxy.md
|
||||
- Graph Traversal: specklepy/objects/graph_traversal/traversal.md
|
||||
- Transports: specklepy/transports/transports.md
|
||||
- Serialization: specklepy/serialization/serializer.md
|
||||
- Core API:
|
||||
- Models: specklepy/core/api/models/models.md
|
||||
- Inputs: specklepy/core/api/inputs/inputs.md
|
||||
- Enums: specklepy/core/api/enums.md
|
||||
- Logging:
|
||||
- Exceptions: specklepy/logging/exceptions.md
|
||||
- Speckle Automate:
|
||||
- AutomationContext: speckle_automate/automation_context.md
|
||||
- Runner: speckle_automate/runner.md
|
||||
- Schema: speckle_automate/schema.md
|
||||
+8
-2
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"appdirs>=1.4.4",
|
||||
"attrs>=24.3.0",
|
||||
"deprecated>=1.2.15",
|
||||
"gql[requests,websockets]>=3.5.0",
|
||||
"gql[requests,websockets]>=3.5.0,<4.0.0",
|
||||
"httpx>=0.28.1",
|
||||
"pydantic>=2.10.5",
|
||||
"pydantic-settings>=2.7.1",
|
||||
@@ -32,11 +32,17 @@ dev = [
|
||||
"pytest-asyncio>=0.25.2",
|
||||
"pytest-cov>=6.0.0",
|
||||
"pytest-ordering>=0.6",
|
||||
"ruff>=0.9.2",
|
||||
"ruff==0.9.2",
|
||||
"types-deprecated>=1.2.15.20241117",
|
||||
"types-requests>=2.32.0.20241016",
|
||||
"types-ujson>=5.10.0.20240515",
|
||||
]
|
||||
docs = [
|
||||
"mkdocs>=1.6.1",
|
||||
"mkdocs-material>=9.6.5",
|
||||
"mkdocstrings>=0.28.1",
|
||||
"mkdocstrings-python>=1.15.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
repository = "https://github.com/specklesystems/specklepy"
|
||||
|
||||
@@ -245,24 +245,24 @@ class AutomationContext:
|
||||
"""
|
||||
)
|
||||
if self.run_status in [AutomationStatus.SUCCEEDED, AutomationStatus.FAILED]:
|
||||
object_results = {
|
||||
"version": 2,
|
||||
results_dict = self._automation_result.model_dump(by_alias=True)
|
||||
results = {
|
||||
"version": 3,
|
||||
"values": {
|
||||
"objectResults": self._automation_result.model_dump(by_alias=True)[
|
||||
"objectResults"
|
||||
],
|
||||
"objectResults": results_dict["objectResults"],
|
||||
"versionResult": results_dict["versionResult"],
|
||||
"blobIds": self._automation_result.blobs,
|
||||
},
|
||||
}
|
||||
else:
|
||||
object_results = None
|
||||
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,
|
||||
"results": results,
|
||||
"contextView": self._automation_result.result_view,
|
||||
}
|
||||
print(f"Reporting run status with content: {params}")
|
||||
@@ -312,25 +312,49 @@ class AutomationContext:
|
||||
|
||||
return upload_response.upload_results[0].blob_id
|
||||
|
||||
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_failed(
|
||||
self, status_message: str, version_result: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Mark the current run a failure.
|
||||
|
||||
Args:
|
||||
status_message: Optional message to be displayed.
|
||||
version_result: Optional data object,
|
||||
that will be attached to the run results.
|
||||
The dictionary should be JSON serializable
|
||||
"""
|
||||
self._mark_run(AutomationStatus.FAILED, status_message, version_result)
|
||||
|
||||
def mark_run_exception(self, status_message: str) -> None:
|
||||
"""Mark the current run a failure."""
|
||||
self._mark_run(AutomationStatus.EXCEPTION, status_message)
|
||||
self._mark_run(AutomationStatus.EXCEPTION, status_message, None)
|
||||
|
||||
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_success(
|
||||
self, status_message: str | None, version_result: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""
|
||||
Mark the current run a success with an optional message.
|
||||
|
||||
Args:
|
||||
status_message: Optional message to be displayed.
|
||||
version_result: Optional data object,
|
||||
that will be attached to the run results.
|
||||
The dictionary should be JSON serializable
|
||||
"""
|
||||
self._mark_run(AutomationStatus.SUCCEEDED, status_message, version_result)
|
||||
|
||||
def _mark_run(
|
||||
self, status: AutomationStatus, status_message: Optional[str]
|
||||
self,
|
||||
status: AutomationStatus,
|
||||
status_message: str | None,
|
||||
version_result: dict[str, Any] | None,
|
||||
) -> None:
|
||||
duration = self.elapsed()
|
||||
self._automation_result.status_message = status_message
|
||||
self._automation_result.run_status = status
|
||||
self._automation_result.elapsed = duration
|
||||
self._automation_result.version_result = version_result
|
||||
|
||||
msg = f"Automation run {status.value} after {duration:.2f} seconds."
|
||||
print("\n".join([msg, status_message]) if status_message else msg)
|
||||
|
||||
@@ -88,10 +88,8 @@ def create_test_automation_run(
|
||||
|
||||
print(result)
|
||||
|
||||
return (
|
||||
result.get("projectMutations")
|
||||
.get("automationMutations")
|
||||
.get("createTestAutomationRun")
|
||||
return TestAutomationRunData.model_validate(
|
||||
result["projectMutations"]["automationMutations"]["createTestAutomationRun"]
|
||||
)
|
||||
|
||||
|
||||
@@ -123,9 +121,9 @@ def create_test_automation_run_data(
|
||||
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"],
|
||||
automation_run_id=test_automation_run_data.automation_run_id,
|
||||
function_run_id=test_automation_run_data.function_run_id,
|
||||
triggers=test_automation_run_data.triggers,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
""""""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Literal, Optional
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from pydantic.alias_generators import to_camel
|
||||
@@ -36,7 +36,7 @@ class AutomationRunData(BaseModel):
|
||||
automation_run_id: str
|
||||
function_run_id: str
|
||||
|
||||
triggers: List[VersionCreationTrigger]
|
||||
triggers: list[VersionCreationTrigger]
|
||||
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
|
||||
@@ -49,7 +49,7 @@ class TestAutomationRunData(BaseModel):
|
||||
automation_run_id: str
|
||||
function_run_id: str
|
||||
|
||||
triggers: List[VersionCreationTrigger]
|
||||
triggers: list[VersionCreationTrigger]
|
||||
|
||||
model_config = ConfigDict(
|
||||
alias_generator=to_camel, populate_by_name=True, protected_namespaces=()
|
||||
@@ -80,19 +80,20 @@ class ResultCase(AutomateBase):
|
||||
|
||||
category: str
|
||||
level: ObjectResultLevel
|
||||
object_app_ids: Dict[str, Optional[str]]
|
||||
message: Optional[str]
|
||||
metadata: Optional[Dict[str, Any]]
|
||||
visual_overrides: Optional[Dict[str, Any]]
|
||||
object_app_ids: dict[str, str | None]
|
||||
message: str | None
|
||||
metadata: dict[str, Any] | None
|
||||
visual_overrides: dict[str, Any] | None
|
||||
|
||||
|
||||
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)
|
||||
result_view: str | None = 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
|
||||
status_message: str | None = None
|
||||
object_results: list[ResultCase] = Field(default_factory=list)
|
||||
version_result: dict[str, Any] | None = None
|
||||
|
||||
@@ -4,21 +4,21 @@ from typing import cast
|
||||
|
||||
from ifcopenshell.ifcopenshell_wrapper import (
|
||||
Triangulation,
|
||||
TriangulationElement,
|
||||
colour,
|
||||
style,
|
||||
)
|
||||
|
||||
from speckleifc.render_material_proxy_manager import RenderMaterialProxyManager
|
||||
from speckleifc.proxy_managers.render_material_proxy_manager import (
|
||||
RenderMaterialProxyManager,
|
||||
)
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.geometry import Mesh
|
||||
from specklepy.objects.other import RenderMaterial
|
||||
|
||||
|
||||
def geometry_to_speckle(
|
||||
shape: TriangulationElement, render_material_manager: RenderMaterialProxyManager
|
||||
geometry: Triangulation, render_material_manager: RenderMaterialProxyManager
|
||||
) -> list[Base]:
|
||||
geometry = cast(Triangulation, shape.geometry)
|
||||
materials = cast(Sequence[style], geometry.materials)
|
||||
MESH_COUNT = max(len(materials), 1)
|
||||
|
||||
@@ -33,7 +33,7 @@ def geometry_to_speckle(
|
||||
# Not really expected, but occasionally some meshes fail to triangulate
|
||||
return []
|
||||
|
||||
mapped_meshes = _pre_alloc_mesh_lists(shape, material_ids, MESH_COUNT)
|
||||
mapped_meshes = _pre_alloc_mesh_lists(geometry, material_ids, MESH_COUNT)
|
||||
for i, mesh in enumerate(mapped_meshes):
|
||||
material = _material_to_speckle(materials[i])
|
||||
render_material_manager.add_mesh_material_mapping(material, mesh)
|
||||
@@ -103,14 +103,14 @@ def _color_to_argb(colour: colour) -> int:
|
||||
|
||||
|
||||
def _pre_alloc_mesh_lists(
|
||||
shape: TriangulationElement, material_ids: Sequence[int], MESH_COUNT: int
|
||||
geometry: Triangulation, material_ids: Sequence[int], MESH_COUNT: int
|
||||
) -> list[Mesh]:
|
||||
"""
|
||||
This is a performance optimisation to pre-size the lists
|
||||
since we're expecting potential hundreds of thousands of verts in a single model
|
||||
This is very much in the hot path, so worth the extra bit of convoluted logic
|
||||
"""
|
||||
appId = cast(str, shape.guid)
|
||||
appId = cast(str, geometry.id)
|
||||
|
||||
material_face_counts = defaultdict(int)
|
||||
for mat_id in material_ids:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import multiprocessing
|
||||
|
||||
from ifcopenshell import file, ifcopenshell_wrapper, open, sqlite
|
||||
from ifcopenshell import SchemaError, file, ifcopenshell_wrapper, open, sqlite
|
||||
from ifcopenshell.geom import iterator, settings
|
||||
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
@@ -12,8 +12,10 @@ def _create_iterator_settings() -> settings:
|
||||
ifc_settings.set("triangulation-type", ifcopenshell_wrapper.TRIANGLE_MESH)
|
||||
# no need to weld verts
|
||||
ifc_settings.set("weld-vertices", False)
|
||||
# Speckle meshes are all in world coords
|
||||
ifc_settings.set("use-world-coords", True)
|
||||
#
|
||||
ifc_settings.set("use-world-coords", False)
|
||||
ifc_settings.set("permissive-shape-reuse", True)
|
||||
|
||||
# Tiny performance improvement,
|
||||
ifc_settings.set("no-wire-intersection-check", True)
|
||||
# Rendermaterials inherit the material names instead of type + unique id
|
||||
@@ -33,7 +35,14 @@ def _create_iterator_settings() -> settings:
|
||||
|
||||
|
||||
def open_ifc(file_path: str) -> file:
|
||||
ifc_file = open(file_path)
|
||||
try:
|
||||
ifc_file = open(file_path)
|
||||
except SchemaError:
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise SpeckleException("File could not be opened as an IFC file") from ex
|
||||
|
||||
if isinstance(ifc_file, file):
|
||||
return ifc_file
|
||||
|
||||
+81
-15
@@ -1,10 +1,10 @@
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import cast
|
||||
from typing import List, cast
|
||||
|
||||
from ifcopenshell.entity_instance import entity_instance
|
||||
from ifcopenshell.geom import file
|
||||
from ifcopenshell.ifcopenshell_wrapper import TriangulationElement
|
||||
from ifcopenshell.ifcopenshell_wrapper import Triangulation, TriangulationElement
|
||||
|
||||
from speckleifc.converter.data_object_converter import data_object_to_speckle
|
||||
from speckleifc.converter.geometry_converter import geometry_to_speckle
|
||||
@@ -12,41 +12,63 @@ from speckleifc.converter.project_converter import project_to_speckle
|
||||
from speckleifc.converter.spatial_element_converter import spatial_element_to_speckle
|
||||
from speckleifc.ifc_geometry_processing import create_geometry_iterator
|
||||
from speckleifc.ifc_openshell_helpers import get_children
|
||||
from speckleifc.level_proxy_manager import LevelProxyManager
|
||||
from speckleifc.render_material_proxy_manager import RenderMaterialProxyManager
|
||||
from speckleifc.proxy_managers.instance_proxy_manager import InstanceProxyManager
|
||||
from speckleifc.proxy_managers.level_proxy_manager import LevelProxyManager
|
||||
from speckleifc.proxy_managers.render_material_proxy_manager import (
|
||||
RenderMaterialProxyManager,
|
||||
)
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.data_objects import DataObject
|
||||
from specklepy.objects.models.collections.collection import Collection
|
||||
from specklepy.objects.proxies import InstanceProxy
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportJob:
|
||||
ifc_file: file
|
||||
cached_display_values: dict[int, list[Base]] = field(default_factory=dict) # noqa: F821
|
||||
|
||||
_render_material_manager: RenderMaterialProxyManager = field(
|
||||
default_factory=lambda: RenderMaterialProxyManager()
|
||||
)
|
||||
_level_proxy_manager: LevelProxyManager = field(
|
||||
default_factory=lambda: LevelProxyManager()
|
||||
)
|
||||
_instance_proxy_manager: InstanceProxyManager = field(
|
||||
default_factory=lambda: InstanceProxyManager()
|
||||
)
|
||||
geometries_count: int = 0
|
||||
geometries_used: int = 0
|
||||
_current_storey_data_object: DataObject | None = field(default=None, init=False)
|
||||
|
||||
_display_value_cache: dict[int, list[Base]] = field(default_factory=dict)
|
||||
"""Maps an instance step ID to a list of instances"""
|
||||
|
||||
def convert_element(self, step_element: entity_instance) -> Base:
|
||||
try:
|
||||
return self._convert_element(step_element)
|
||||
except SpeckleException:
|
||||
raise
|
||||
except Exception as ex:
|
||||
raise SpeckleException(
|
||||
f"Failed to convert {step_element.is_a()} #{step_element.id()}"
|
||||
) from ex
|
||||
|
||||
def _convert_element(self, step_element: entity_instance) -> Base:
|
||||
# Track current storey context and store for level proxies
|
||||
previous_storey_data_object = self._current_storey_data_object
|
||||
if step_element.is_a("IfcBuildingStorey"):
|
||||
# Convert the building storey to a DataObject for the level proxy
|
||||
storey_display_value = self.cached_display_values.get(step_element.id(), [])
|
||||
storey_display_value = self._display_value_cache.get(step_element.id(), [])
|
||||
self._current_storey_data_object = data_object_to_speckle(
|
||||
storey_display_value, step_element, []
|
||||
)
|
||||
|
||||
children = self._convert_children(step_element)
|
||||
display_value = self.cached_display_values.get(step_element.id(), [])
|
||||
id = step_element.id()
|
||||
display_value = self._display_value_cache.get(id, [])
|
||||
|
||||
if display_value is not None:
|
||||
if display_value:
|
||||
self.geometries_used += 1
|
||||
|
||||
# Extract current storey name from DataObject if available
|
||||
@@ -110,21 +132,53 @@ class ImportJob:
|
||||
def pre_process_geometry(self) -> None:
|
||||
iterator = create_geometry_iterator(self.ifc_file)
|
||||
if not iterator.initialize():
|
||||
raise SpeckleException(
|
||||
"geometry iterator failed to initialize for the given file"
|
||||
)
|
||||
raise SpeckleException("Failed to find any geometry in file")
|
||||
self.geometries_count = 0
|
||||
while True:
|
||||
shape = cast(TriangulationElement, iterator.get())
|
||||
self.geometries_count += 1
|
||||
id = cast(int, shape.id)
|
||||
|
||||
display_value = geometry_to_speckle(shape, self._render_material_manager)
|
||||
self.cached_display_values[id] = display_value
|
||||
|
||||
try:
|
||||
display_value = self._create_display_value(shape)
|
||||
self._display_value_cache[id] = display_value
|
||||
except Exception as ex:
|
||||
raise SpeckleException(
|
||||
f"Failed to convert geometry with id: {id}"
|
||||
) from ex
|
||||
if not iterator.next():
|
||||
break
|
||||
|
||||
def _create_display_value(self, shape: TriangulationElement) -> List[Base]:
|
||||
geometry = cast(Triangulation, shape.geometry)
|
||||
display_value_geometry = geometry_to_speckle(
|
||||
geometry, self._render_material_manager
|
||||
)
|
||||
|
||||
definition_ids = self._instance_proxy_manager.add_display_value_definitions(
|
||||
display_value_geometry
|
||||
)
|
||||
matrix = shape.transformation.matrix
|
||||
transposed = [
|
||||
matrix[0], matrix[4], matrix[8], matrix[12],
|
||||
matrix[1], matrix[5], matrix[9], matrix[13],
|
||||
matrix[2], matrix[6], matrix[10], matrix[14],
|
||||
matrix[3], matrix[7], matrix[11], matrix[15],
|
||||
] # fmt: skip
|
||||
|
||||
return [
|
||||
cast(
|
||||
Base,
|
||||
InstanceProxy(
|
||||
units="m",
|
||||
definitionId=definition_id,
|
||||
transform=transposed,
|
||||
maxDepth=0,
|
||||
applicationId=f"{shape.guid}:{definition_id}",
|
||||
),
|
||||
)
|
||||
for definition_id in definition_ids
|
||||
]
|
||||
|
||||
def _convert_project_tree(self) -> Base:
|
||||
projects = self.ifc_file.by_type("IfcProject", False)
|
||||
if len(projects) != 1:
|
||||
@@ -132,10 +186,22 @@ class ImportJob:
|
||||
project = projects[0]
|
||||
|
||||
tree = self.convert_element(project)
|
||||
if not isinstance(tree, Collection):
|
||||
raise TypeError("Expected root object to convert to a Collection")
|
||||
|
||||
tree["renderMaterialProxies"] = list(
|
||||
self._render_material_manager.render_material_proxies.values()
|
||||
)
|
||||
tree["levelProxies"] = list(self._level_proxy_manager.level_proxies.values())
|
||||
tree["instanceDefinitionProxies"] = list(
|
||||
self._instance_proxy_manager.instance_definition_proxies.values()
|
||||
)
|
||||
tree.elements.append(
|
||||
Collection(
|
||||
name="definitionGeometry",
|
||||
elements=list(self._instance_proxy_manager.instance_geometry.values()),
|
||||
)
|
||||
)
|
||||
tree["version"] = 3
|
||||
|
||||
return tree
|
||||
|
||||
@@ -26,6 +26,7 @@ def open_and_convert_file(
|
||||
remote_transport = ServerTransport(project.id, account=account)
|
||||
|
||||
ifc_file = open_ifc(file_path) # pyright: ignore[reportUnknownVariableType]
|
||||
|
||||
import_job = ImportJob(ifc_file) # pyright: ignore[reportUnknownArgumentType]
|
||||
data = import_job.convert()
|
||||
|
||||
@@ -55,6 +56,6 @@ def open_and_convert_file(
|
||||
custom_properties = {"ui": "dui3", "actionSource": "import"}
|
||||
if project.workspace_id:
|
||||
custom_properties["workspace_id"] = project.workspace_id
|
||||
metrics.track(metrics.SEND, account, custom_properties)
|
||||
metrics.track(metrics.SEND, account, custom_properties, send_sync=True)
|
||||
|
||||
return version
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import math
|
||||
from typing import Any, Tuple
|
||||
|
||||
from ifcopenshell.entity_instance import entity_instance
|
||||
@@ -134,6 +135,10 @@ def _get_quantities(
|
||||
value = getattr(quantity, quantity.attribute_name(3))
|
||||
unit_info = _get_unit_info(element, quantity)
|
||||
|
||||
# Server does not consider `NaN` valid json
|
||||
if math.isnan(value):
|
||||
value = None
|
||||
|
||||
if unit_info:
|
||||
# Create structured quantity object with units
|
||||
results[quantity_name] = {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
from typing import Sequence
|
||||
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.proxies import InstanceDefinitionProxy
|
||||
|
||||
|
||||
class InstanceProxyManager:
|
||||
def __init__(self):
|
||||
self._instance_definition_proxies: dict[str, InstanceDefinitionProxy] = {}
|
||||
"""definition proxies to be added directly to the root"""
|
||||
self._instance_geometry: dict[str, Base] = {}
|
||||
"""The geometry that will be added in it's own collection under the root"""
|
||||
|
||||
@property
|
||||
def instance_definition_proxies(self) -> dict[str, InstanceDefinitionProxy]:
|
||||
return self._instance_definition_proxies
|
||||
|
||||
@property
|
||||
def instance_geometry(self) -> dict[str, Base]:
|
||||
return self._instance_geometry
|
||||
|
||||
def add_display_value_definitions(self, geometry: Sequence[Base]) -> list[str]:
|
||||
result: list[str] = []
|
||||
for m in geometry:
|
||||
if not m.applicationId:
|
||||
raise ValueError("geometry with no applicationId cannot be proxied ")
|
||||
definition_id = f"DEFINITION:{m.applicationId}"
|
||||
result.append(definition_id)
|
||||
self._add_definition(definition_id, [m.applicationId], 0)
|
||||
self._instance_geometry[m.applicationId] = m
|
||||
|
||||
return result
|
||||
|
||||
def _add_definition(
|
||||
self, definition_id: str, objects: list[str], max_depth: int
|
||||
) -> None:
|
||||
proxy = InstanceDefinitionProxy(
|
||||
applicationId=definition_id,
|
||||
name=definition_id,
|
||||
objects=objects,
|
||||
maxDepth=max_depth,
|
||||
)
|
||||
self._instance_definition_proxies[definition_id] = proxy
|
||||
@@ -131,6 +131,19 @@ class SpeckleClient:
|
||||
self.account = Account.from_token(token, self.url)
|
||||
self._set_up_client()
|
||||
|
||||
userData = self.active_user.get()
|
||||
|
||||
# None if the token lacked the profile:read scope or if it was None
|
||||
if userData:
|
||||
self.account.userInfo.id = userData.id
|
||||
self.account.userInfo.email = userData.email
|
||||
self.account.userInfo.name = userData.name
|
||||
self.account.userInfo.company = userData.company
|
||||
self.account.userInfo.avatar = userData.avatar
|
||||
|
||||
self.account.serverInfo = self.server.get()
|
||||
self.account.serverInfo.url = self.url
|
||||
|
||||
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
|
||||
@@ -143,6 +156,21 @@ class SpeckleClient:
|
||||
self.account = account
|
||||
self._set_up_client()
|
||||
|
||||
try:
|
||||
_ = self.active_user.get()
|
||||
except SpeckleException as ex:
|
||||
if isinstance(ex.exception, TransportServerError):
|
||||
if ex.exception.code == 403:
|
||||
warn(
|
||||
SpeckleWarning(
|
||||
"Possibly invalid token - could not authenticate "
|
||||
f"Speckle Client for server {self.url}"
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
raise ex
|
||||
|
||||
def _set_up_client(self) -> None:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.account.token}",
|
||||
@@ -162,21 +190,6 @@ class SpeckleClient:
|
||||
|
||||
self._init_resources()
|
||||
|
||||
try:
|
||||
_ = self.active_user.get()
|
||||
except SpeckleException as ex:
|
||||
if isinstance(ex.exception, TransportServerError):
|
||||
if ex.exception.code == 403:
|
||||
warn(
|
||||
SpeckleWarning(
|
||||
"Possibly invalid token - could not authenticate "
|
||||
f"Speckle Client for server {self.url}"
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
else:
|
||||
raise ex
|
||||
|
||||
def execute_query(self, query: str) -> Dict:
|
||||
return self.httpclient.execute(query)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class Account(BaseModel):
|
||||
return self.__repr__()
|
||||
|
||||
@classmethod
|
||||
def from_token(cls, token: str, server_url: str = None):
|
||||
def from_token(cls, token: str, server_url: str | None = None):
|
||||
acct = cls(token=token)
|
||||
acct.serverInfo.url = server_url
|
||||
return acct
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
from warnings import warn
|
||||
|
||||
from gql import gql
|
||||
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.api.credentials import (
|
||||
Account,
|
||||
@@ -139,27 +137,11 @@ class StreamWrapper:
|
||||
|
||||
if use_fe2 is True and self.branch_name is not None:
|
||||
self.model_id = self.branch_name
|
||||
# get branch name
|
||||
query = gql(
|
||||
"""
|
||||
query Project($project_id: String!, $model_id: String!) {
|
||||
project(id: $project_id) {
|
||||
id
|
||||
model(id: $model_id) {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
self._client = self.get_client()
|
||||
params = {"project_id": self.stream_id, "model_id": self.model_id}
|
||||
project = self._client.httpclient.execute(query, params)
|
||||
|
||||
try:
|
||||
self.branch_name = project["project"]["model"]["name"]
|
||||
except KeyError as ke:
|
||||
raise SpeckleException("Project model name is not found", ke) from ke
|
||||
self._client = self.get_client()
|
||||
model = self._client.model.get(self.model_id, self.stream_id)
|
||||
|
||||
self.branch_name = model.name
|
||||
|
||||
if not self.stream_id:
|
||||
raise SpeckleException(
|
||||
@@ -175,6 +157,10 @@ class StreamWrapper:
|
||||
"""
|
||||
Gets an account object for this server from the local accounts db
|
||||
(added via Speckle Manager or a json file)
|
||||
|
||||
WARNING: this function will return ANY account for the server,
|
||||
just because you pass a token in doesn't guarantee it will be used.
|
||||
This whole class could do with a re-design...
|
||||
"""
|
||||
if self._account and self._account.token:
|
||||
return self._account
|
||||
|
||||
@@ -88,6 +88,8 @@ def user_application_data_path() -> Path:
|
||||
message="Cannot get appdata path from environment."
|
||||
)
|
||||
return Path(app_data_path)
|
||||
if sys.platform.startswith("darwin"): # macOS
|
||||
return _ensure_folder_exists(Path.home() / "Library", "Application Support")
|
||||
else:
|
||||
# try getting the standard XDG_DATA_HOME value
|
||||
# as that is used as an override
|
||||
@@ -98,7 +100,7 @@ def user_application_data_path() -> Path:
|
||||
return _ensure_folder_exists(Path.home(), ".config")
|
||||
except Exception as ex:
|
||||
raise SpeckleException(
|
||||
message="Failed to initialize user application data path.", exception=ex
|
||||
message="Failed to initialize user application data path."
|
||||
) from ex
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import platform
|
||||
import queue
|
||||
import sys
|
||||
import threading
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
@@ -29,21 +30,6 @@ CONNECTOR = "Connector Action"
|
||||
RECEIVE = "Receive"
|
||||
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_WRAPPER = "Stream Wrapper"
|
||||
USER = "User Action"
|
||||
|
||||
|
||||
def disable():
|
||||
global TRACK
|
||||
@@ -65,43 +51,44 @@ def track(
|
||||
action: str,
|
||||
account: Account | None = None,
|
||||
custom_props: dict | None = None,
|
||||
send_sync: bool = False,
|
||||
):
|
||||
if not TRACK:
|
||||
return
|
||||
try:
|
||||
initialise_tracker(account)
|
||||
event_params = {
|
||||
"event": action,
|
||||
"properties": {
|
||||
"distinct_id": METRICS_TRACKER.last_user,
|
||||
"server_id": METRICS_TRACKER.last_server,
|
||||
"token": METRICS_TRACKER.analytics_token,
|
||||
"hostApp": HOST_APP,
|
||||
"hostAppVersion": HOST_APP_VERSION,
|
||||
"$os": METRICS_TRACKER.platform,
|
||||
"type": "action",
|
||||
},
|
||||
}
|
||||
if custom_props:
|
||||
event_params["properties"].update(custom_props)
|
||||
|
||||
METRICS_TRACKER.queue.put_nowait(event_params)
|
||||
except Exception as ex:
|
||||
# wrapping this whole thing in a try except as we never want a failure here
|
||||
# to annoy users!
|
||||
LOG.debug(f"Error queueing metrics request: {str(ex)}")
|
||||
tracker = initialise_tracker(account)
|
||||
event_params: dict[str, Any] = {
|
||||
"event": action,
|
||||
"properties": {
|
||||
"distinct_id": tracker.last_user,
|
||||
"server_id": tracker.last_server,
|
||||
"token": tracker.analytics_token,
|
||||
"hostApp": HOST_APP,
|
||||
"hostAppVersion": HOST_APP_VERSION,
|
||||
"$os": tracker.platform,
|
||||
"type": "action",
|
||||
},
|
||||
}
|
||||
if custom_props:
|
||||
event_params["properties"].update(custom_props)
|
||||
|
||||
if send_sync:
|
||||
tracker.send_event(event_params)
|
||||
else:
|
||||
tracker.queue_event(event_params)
|
||||
|
||||
|
||||
def initialise_tracker(account: Account | None = None):
|
||||
def initialise_tracker(account: Account | None = None) -> "MetricsTracker":
|
||||
global METRICS_TRACKER
|
||||
if not METRICS_TRACKER:
|
||||
METRICS_TRACKER = MetricsTracker()
|
||||
|
||||
if account and account.userInfo.email:
|
||||
if account:
|
||||
METRICS_TRACKER.set_last_user(account.userInfo.email)
|
||||
if account and account.serverInfo.url:
|
||||
METRICS_TRACKER.set_last_server(account.serverInfo.url)
|
||||
|
||||
return METRICS_TRACKER
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
@@ -113,48 +100,62 @@ class Singleton(type):
|
||||
|
||||
|
||||
class MetricsTracker(metaclass=Singleton):
|
||||
analytics_url = "https://analytics.speckle.systems/track?ip=1"
|
||||
analytics_token = "acd87c5a50b56df91a795e999812a3a4"
|
||||
last_user = ""
|
||||
last_server = None
|
||||
platform = None
|
||||
sending_thread = None
|
||||
queue = queue.Queue(1000)
|
||||
analytics_url: str = "https://analytics.speckle.systems/track?ip=1"
|
||||
analytics_token: str = "acd87c5a50b56df91a795e999812a3a4"
|
||||
last_user: str = ""
|
||||
last_server: str | None = None
|
||||
platform: str
|
||||
|
||||
_sending_thread: threading.Thread
|
||||
_queue: queue.Queue[dict[str, Any]] = queue.Queue(1000)
|
||||
_session = requests.Session()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sending_thread = threading.Thread(
|
||||
self._sending_thread = threading.Thread(
|
||||
target=self._send_tracking_requests, daemon=True
|
||||
)
|
||||
self.platform = PLATFORMS.get(sys.platform, "linux")
|
||||
self.sending_thread.start()
|
||||
self._sending_thread.start()
|
||||
with contextlib.suppress(Exception):
|
||||
node, user = platform.node(), getpass.getuser()
|
||||
if node and user:
|
||||
self.last_user = f"@{self.hash(f'{node}-{user}')}"
|
||||
|
||||
def set_last_user(self, email: str):
|
||||
def set_last_user(self, email: str | None) -> None:
|
||||
if not email:
|
||||
return
|
||||
self.last_user = f"@{self.hash(email)}"
|
||||
|
||||
def set_last_server(self, server: str):
|
||||
def set_last_server(self, server: str | None) -> None:
|
||||
if not server:
|
||||
return
|
||||
self.last_server = self.hash(server)
|
||||
|
||||
def hash(self, value: str):
|
||||
def hash(self, value: str) -> str:
|
||||
inputList = value.lower().split("://")
|
||||
input = inputList[len(inputList) - 1].split("/")[0].split("?")[0]
|
||||
return hashlib.md5(input.encode("utf-8")).hexdigest().upper()
|
||||
|
||||
def _send_tracking_requests(self):
|
||||
session = requests.Session()
|
||||
def queue_event(self, event_params: dict[str, Any]) -> None:
|
||||
try:
|
||||
self._queue.put_nowait(event_params)
|
||||
except queue.Full:
|
||||
LOG.warning(
|
||||
"Metrics event was skipped because the metrics queue was was full",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
def send_event(self, event_params: dict[str, Any]) -> None:
|
||||
response = self._session.post(self.analytics_url, json=[event_params])
|
||||
response.raise_for_status()
|
||||
|
||||
def _send_tracking_requests(self) -> None:
|
||||
while True:
|
||||
event_params = [self.queue.get()]
|
||||
event_params = self._queue.get()
|
||||
|
||||
try:
|
||||
session.post(self.analytics_url, json=event_params)
|
||||
except Exception as ex:
|
||||
LOG.debug(f"Error sending metrics request: {str(ex)}")
|
||||
self.send_event(event_params)
|
||||
except Exception:
|
||||
LOG.warning("Error sending metrics request", exc_info=True)
|
||||
|
||||
self.queue.task_done()
|
||||
self._queue.task_done()
|
||||
|
||||
@@ -323,6 +323,30 @@ def _validate_type(t: Optional[type], value: Any) -> Tuple[bool, Any]:
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Base(_RegisteringBase, speckle_type="Base"):
|
||||
"""Base class for all Speckle objects.
|
||||
|
||||
The base object class is the foundation of all data being
|
||||
transferred with Speckle. Any custom data structure that you want to transfer via
|
||||
Speckle should inherit from it.
|
||||
|
||||
Objects in Speckle are immutable for storage purposes. When any property changes,
|
||||
the object gets a new identity (hash). This hash is stored in the `id` property
|
||||
after serialization.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier (hash) for the object. This is typically
|
||||
set automatically during serialization and depends on the object's properties.
|
||||
applicationId: Optional identifier for the application that created
|
||||
this object, can store the host application's native object ID.
|
||||
|
||||
```py title="Example"
|
||||
from specklepy.objects.base import Base
|
||||
obj = Base(id="some-id", applicationId="my-app")
|
||||
obj["custom_prop"] = 42 # Add a dynamic property
|
||||
obj["@detached_prop"] = another_object # Add a detached property
|
||||
```
|
||||
"""
|
||||
|
||||
id: Union[str, None] = None
|
||||
# totalChildrenCount: Union[int, None] = None
|
||||
applicationId: Union[str, None] = None
|
||||
|
||||
@@ -18,6 +18,10 @@ class DataObject(
|
||||
speckle_type="Objects.Data.DataObject",
|
||||
detachable={"displayValue"},
|
||||
):
|
||||
"""
|
||||
A generic data object that can hold arbitrary properties and display values.
|
||||
"""
|
||||
|
||||
name: str
|
||||
properties: Dict[str, object]
|
||||
displayValue: List[Base]
|
||||
|
||||
@@ -9,6 +9,30 @@ from specklepy.objects.interfaces import ICurve, IHasUnits
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class Arc(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Arc"):
|
||||
"""
|
||||
An arc defined by a plane, start point, mid point and end point.
|
||||
|
||||
This class represents a circular arc in 3D space, defined by three points
|
||||
and a plane. The arc is a portion of a circle that lies on the specified plane.
|
||||
|
||||
Attributes:
|
||||
plane: The plane on which the arc lies
|
||||
startPoint: The starting point of the arc
|
||||
midPoint: A point on the arc between the start and end points
|
||||
endPoint: The ending point of the arc.
|
||||
|
||||
|
||||
```py title="Example"
|
||||
from specklepy.objects.geometry.plane import Plane
|
||||
from specklepy.objects.geometry.point import Point
|
||||
plane = Plane(origin=Point(0, 0, 0), normal=Point(0, 0, 1))
|
||||
start = Point(1, 0, 0)
|
||||
mid = Point(0.7071, 0.7071, 0)
|
||||
end = Point(0, 1, 0)
|
||||
arc = Arc(plane=plane, startPoint=start, midPoint=mid, endPoint=end)
|
||||
```
|
||||
"""
|
||||
|
||||
plane: Plane
|
||||
startPoint: Point
|
||||
midPoint: Point
|
||||
@@ -16,10 +40,20 @@ class Arc(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Arc"):
|
||||
|
||||
@property
|
||||
def radius(self) -> float:
|
||||
"""Calculates the radius of the arc.
|
||||
|
||||
Returns:
|
||||
The radius of the arc, as the distance from the start point to the origin.
|
||||
"""
|
||||
return self.startPoint.distance_to(self.plane.origin)
|
||||
|
||||
@property
|
||||
def length(self) -> float:
|
||||
"""Calculates the length of the arc.
|
||||
|
||||
Returns:
|
||||
The length of the arc.
|
||||
"""
|
||||
start_to_mid = self.startPoint.distance_to(self.midPoint)
|
||||
mid_to_end = self.midPoint.distance_to(self.endPoint)
|
||||
r = self.radius
|
||||
@@ -30,6 +64,11 @@ class Arc(Base, IHasUnits, ICurve, speckle_type="Objects.Geometry.Arc"):
|
||||
|
||||
@property
|
||||
def measure(self) -> float:
|
||||
"""Calculates the angular measure of the arc in radians.
|
||||
|
||||
Returns:
|
||||
The angular measure of the arc in radians.
|
||||
"""
|
||||
start_to_mid = self.startPoint.distance_to(self.midPoint)
|
||||
mid_to_end = self.midPoint.distance_to(self.endPoint)
|
||||
r = self.radius
|
||||
|
||||
@@ -9,7 +9,29 @@ from specklepy.objects.primitive import Interval
|
||||
@dataclass(kw_only=True)
|
||||
class Box(Base, IHasUnits, IHasArea, IHasVolume, speckle_type="Objects.Geometry.Box"):
|
||||
"""
|
||||
a 3-dimensional box oriented on a plane
|
||||
A 3-dimensional box oriented on a plane.
|
||||
|
||||
This class represents a rectangular prism in 3D space, defined by a base plane and
|
||||
three intervals specifying its dimensions along the x, y, and z axes.
|
||||
|
||||
Attributes:
|
||||
basePlane: The plane on which the box is oriented
|
||||
xSize: The interval defining the box's size along the x-axis
|
||||
ySize: The interval defining the box's size along the y-axis
|
||||
zSize: The interval defining the box's size along the z-axis
|
||||
|
||||
```py title="Example"
|
||||
from specklepy.objects.geometry.plane import Plane
|
||||
from specklepy.objects.geometry.point import Point
|
||||
from specklepy.objects.primitive import Interval
|
||||
|
||||
base_plane = Plane(origin=Point(0, 0, 0), normal=Point(0, 0, 1))
|
||||
x_size = Interval(start=0, end=10)
|
||||
y_size = Interval(start=0, end=5)
|
||||
z_size = Interval(start=0, end=3)
|
||||
|
||||
box = Box(basePlane=base_plane, xSize=x_size, ySize=y_size, zSize=z_size)
|
||||
```
|
||||
"""
|
||||
|
||||
basePlane: Plane
|
||||
@@ -29,6 +51,11 @@ class Box(Base, IHasUnits, IHasArea, IHasVolume, speckle_type="Objects.Geometry.
|
||||
|
||||
@property
|
||||
def area(self) -> float:
|
||||
"""Calculates the surface area of the box.
|
||||
|
||||
Returns:
|
||||
The total surface area of the box.
|
||||
"""
|
||||
return 2 * (
|
||||
self.xSize.length * self.ySize.length
|
||||
+ self.xSize.length * self.zSize.length
|
||||
@@ -37,4 +64,9 @@ class Box(Base, IHasUnits, IHasArea, IHasVolume, speckle_type="Objects.Geometry.
|
||||
|
||||
@property
|
||||
def volume(self) -> float:
|
||||
"""Calculates the volume of the box.
|
||||
|
||||
Returns:
|
||||
The volume of the box.
|
||||
"""
|
||||
return self.xSize.length * self.ySize.length * self.zSize.length
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from specklepy.objects.graph_traversal.traversal import (
|
||||
DefaultRule,
|
||||
GraphTraversal,
|
||||
TraversalContext,
|
||||
TraversalRule,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"GraphTraversal",
|
||||
"TraversalContext",
|
||||
"TraversalRule",
|
||||
"DefaultRule",
|
||||
]
|
||||
@@ -23,6 +23,9 @@ class ITraversalRule(Protocol):
|
||||
@final
|
||||
@define(slots=True, frozen=True)
|
||||
class DefaultRule:
|
||||
def should_return(self) -> bool:
|
||||
return False
|
||||
|
||||
def get_members_to_traverse(self, _) -> Set[str]:
|
||||
return set()
|
||||
|
||||
@@ -47,6 +50,15 @@ class TraversalContext:
|
||||
class GraphTraversal:
|
||||
_rules: List[ITraversalRule]
|
||||
|
||||
def _get_active_rule(self, o: Base) -> Optional[ITraversalRule]:
|
||||
for rule in self._rules:
|
||||
if rule.does_rule_hold(o):
|
||||
return rule
|
||||
return None
|
||||
|
||||
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
|
||||
return self._get_active_rule(o) or _default_rule
|
||||
|
||||
def traverse(self, root: Base) -> Iterator[TraversalContext]:
|
||||
stack: List[TraversalContext] = []
|
||||
|
||||
@@ -110,15 +122,6 @@ class GraphTraversal:
|
||||
for o in GraphTraversal.traverse_member(obj):
|
||||
yield o
|
||||
|
||||
def _get_active_rule_or_default_rule(self, o: Base) -> ITraversalRule:
|
||||
return self._get_active_rule(o) or _default_rule
|
||||
|
||||
def _get_active_rule(self, o: Base) -> Optional[ITraversalRule]:
|
||||
for rule in self._rules:
|
||||
if rule.does_rule_hold(o):
|
||||
return rule
|
||||
return None
|
||||
|
||||
|
||||
@final
|
||||
@define(slots=True, frozen=True)
|
||||
|
||||
@@ -33,9 +33,9 @@ class InstanceProxy(
|
||||
IHasUnits,
|
||||
speckle_type="Speckle.Core.Models.Instances.InstanceProxy",
|
||||
):
|
||||
definition_id: str
|
||||
definitionId: str
|
||||
transform: List[float]
|
||||
max_depth: int
|
||||
maxDepth: int
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
@@ -45,7 +45,7 @@ class InstanceDefinitionProxy(
|
||||
detachable={"objects"},
|
||||
):
|
||||
objects: List[str]
|
||||
max_depth: int
|
||||
maxDepth: int
|
||||
name: str
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import threading
|
||||
import requests
|
||||
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
from specklepy.transports.server.retry_policy import setup_session
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@@ -72,10 +73,7 @@ class BatchSender:
|
||||
|
||||
def _sending_thread_main(self):
|
||||
try:
|
||||
session = requests.Session()
|
||||
session.headers.update(
|
||||
{"Authorization": f"Bearer {self._token}", "Accept": "text/plain"}
|
||||
)
|
||||
session = setup_session(self._token)
|
||||
|
||||
while True:
|
||||
batch = self._batches.get()
|
||||
@@ -123,8 +121,8 @@ class BatchSender:
|
||||
upload_data = "[" + ",".join(new_objects) + "]"
|
||||
upload_data_gzip = gzip.compress(upload_data.encode())
|
||||
LOG.info(
|
||||
"Uploading batch of {batch_size} objects {new_object_count}: ",
|
||||
"(size: {upload_size}, compressed size: {upload_data_size})",
|
||||
"Uploading batch of {batch_size} objects {new_object_count}: "
|
||||
+ "(size: {upload_size}, compressed size: {upload_data_size})",
|
||||
{
|
||||
"batch_size": len(batch),
|
||||
"new_object_count": len(new_objects),
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import requests
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3 import Retry
|
||||
|
||||
|
||||
def setup_session(auth_token: str | None) -> requests.Session:
|
||||
"""
|
||||
Sets up a requests.Session with a basic retry policy
|
||||
to retry on all the usual retryable status codes, with a back off policy:
|
||||
1st: 0ms,
|
||||
2nd: 500ms,
|
||||
3rd: 1500ms.
|
||||
|
||||
Also sets "Accept": "text/plain" header (because this is what ServerTransport needs)
|
||||
and (if a auth_token is provided) the Authorization header
|
||||
"""
|
||||
|
||||
session = requests.Session()
|
||||
retry_policy = Retry(
|
||||
total=3,
|
||||
read=3,
|
||||
connect=3,
|
||||
backoff_factor=0.5,
|
||||
status_forcelist=(500, 502, 503, 504, 520, 408, 429),
|
||||
allowed_methods=["HEAD", "GET", "OPTIONS", "POST", "PUT", "DELETE"],
|
||||
raise_on_status=False,
|
||||
)
|
||||
|
||||
adapter = HTTPAdapter(max_retries=retry_policy)
|
||||
session.mount("https://", adapter)
|
||||
session.mount("http://", adapter)
|
||||
|
||||
session.headers.update(
|
||||
{
|
||||
"Accept": "text/plain",
|
||||
}
|
||||
)
|
||||
|
||||
if auth_token is not None:
|
||||
session.headers.update(
|
||||
{
|
||||
"Authorization": f"Bearer {auth_token}",
|
||||
}
|
||||
)
|
||||
|
||||
return session
|
||||
@@ -2,12 +2,11 @@ import json
|
||||
from typing import Dict, List, Optional
|
||||
from warnings import warn
|
||||
|
||||
import requests
|
||||
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.api.credentials import Account, get_account_from_token
|
||||
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
||||
from specklepy.transports.abstract_transport import AbstractTransport
|
||||
from specklepy.transports.server.retry_policy import setup_session
|
||||
|
||||
from .batch_sender import BatchSender
|
||||
|
||||
@@ -92,23 +91,13 @@ class ServerTransport(AbstractTransport):
|
||||
self.stream_id = stream_id
|
||||
self.url = url
|
||||
|
||||
self.session = requests.Session()
|
||||
|
||||
self.session.headers.update(
|
||||
{
|
||||
"Accept": "text/plain",
|
||||
}
|
||||
)
|
||||
|
||||
if self.account is not None:
|
||||
self._batch_sender = BatchSender(
|
||||
self.url, self.stream_id, self.account.token, max_batch_size_mb=1
|
||||
)
|
||||
self.session.headers.update(
|
||||
{
|
||||
"Authorization": f"Bearer {self.account.token}",
|
||||
}
|
||||
)
|
||||
self.session = setup_session(
|
||||
self.account.token if self.account is not None else None
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
@@ -6,7 +6,7 @@ from specklepy.api import operations
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.credentials import Account, get_account_from_token
|
||||
from specklepy.core.helpers import speckle_path_provider
|
||||
from specklepy.logging.exceptions import SpeckleException, SpeckleWarning
|
||||
from specklepy.logging.exceptions import SpeckleException
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.transports.server import ServerTransport
|
||||
|
||||
@@ -17,7 +17,7 @@ def test_invalid_authentication():
|
||||
speckle_path_provider.override_application_data_path(gettempdir())
|
||||
client = SpeckleClient()
|
||||
|
||||
with pytest.warns(SpeckleWarning):
|
||||
with pytest.raises(SpeckleException):
|
||||
client.authenticate_with_token("fake token")
|
||||
|
||||
# remove path override
|
||||
|
||||
@@ -8,11 +8,12 @@ import requests
|
||||
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.core.api import operations
|
||||
from specklepy.core.api.credentials import Account, UserInfo
|
||||
from specklepy.core.api.enums import ProjectVisibility
|
||||
from specklepy.core.api.inputs.project_inputs import ProjectCreateInput
|
||||
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
|
||||
from specklepy.core.api.models import Version
|
||||
from specklepy.core.api.models.current import Project
|
||||
from specklepy.core.api.models.current import Project, ServerInfo
|
||||
from specklepy.logging import metrics
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.objects.geometry import Point
|
||||
@@ -89,13 +90,15 @@ def second_user_dict(host: str) -> Dict[str, str]:
|
||||
def create_client(host: str, token: str) -> SpeckleClient:
|
||||
client = SpeckleClient(host=host, use_ssl=False)
|
||||
client.authenticate_with_token(token)
|
||||
user = client.active_user.get()
|
||||
assert user
|
||||
client.account.userInfo.id = user.id
|
||||
client.account.userInfo.email = user.email
|
||||
client.account.userInfo.name = user.name
|
||||
client.account.userInfo.company = user.company
|
||||
client.account.userInfo.avatar = user.avatar
|
||||
|
||||
assert isinstance(client.account, Account)
|
||||
assert isinstance(client.account.userInfo, UserInfo)
|
||||
assert client.account.userInfo.id
|
||||
assert client.account.userInfo.name
|
||||
assert isinstance(client.account.serverInfo, ServerInfo)
|
||||
assert client.account.serverInfo.url
|
||||
assert client.account.serverInfo.name
|
||||
|
||||
return client
|
||||
|
||||
|
||||
|
||||
@@ -14,10 +14,14 @@ from speckle_automate import (
|
||||
run_function,
|
||||
)
|
||||
from speckle_automate.fixtures import (
|
||||
TestAutomationEnvironment,
|
||||
create_test_automation_run_data,
|
||||
)
|
||||
from speckle_automate.schema import AutomateBase
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.core.api.enums import ProjectVisibility
|
||||
from specklepy.core.api.inputs import ProjectCreateInput
|
||||
from specklepy.core.api.models import Project
|
||||
from specklepy.core.api.models.current import Model, Version
|
||||
from specklepy.core.helpers import crypto_random_string
|
||||
from specklepy.objects.base import Base
|
||||
@@ -43,18 +47,33 @@ def test_client(speckle_server_url: str, speckle_token: str) -> SpeckleClient:
|
||||
return test_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project(test_client: SpeckleClient) -> Project:
|
||||
return test_client.project.create(
|
||||
ProjectCreateInput(
|
||||
name="test", description=None, visibility=ProjectVisibility.PRIVATE
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def automation_run_data(
|
||||
test_client: SpeckleClient, speckle_server_url: str
|
||||
test_client: SpeckleClient,
|
||||
speckle_server_url: str,
|
||||
speckle_token: str,
|
||||
project: Project,
|
||||
) -> AutomationRunData:
|
||||
"""TODO: Set up a test automation for integration testing"""
|
||||
project_id = crypto_random_string(10)
|
||||
test_automation_id = crypto_random_string(10)
|
||||
|
||||
return create_test_automation_run_data(
|
||||
test_client, speckle_server_url, project_id, test_automation_id
|
||||
environment = TestAutomationEnvironment(
|
||||
token=speckle_token,
|
||||
server_url=speckle_server_url,
|
||||
project_id=project.id,
|
||||
automation_id=test_automation_id,
|
||||
)
|
||||
|
||||
return create_test_automation_run_data(test_client, environment)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def automation_context(
|
||||
@@ -133,7 +152,7 @@ def automate_function(
|
||||
raise ValueError("Cannot operate on objects without their id's.")
|
||||
automation_context.attach_error_to_objects(
|
||||
"Forbidden speckle_type",
|
||||
version_root_object.id,
|
||||
version_root_object,
|
||||
"This project should not contain the type: "
|
||||
f"{function_inputs.forbidden_speckle_type}",
|
||||
)
|
||||
@@ -164,7 +183,7 @@ def test_function_run(automation_context: AutomationContext) -> None:
|
||||
assert automation_context.run_status == AutomationStatus.FAILED
|
||||
status = get_automation_status(
|
||||
automation_context.automation_run_data.project_id,
|
||||
automation_context.automation_run_data.model_id,
|
||||
automation_context.automation_run_data.triggers[0].payload.model_id,
|
||||
automation_context.speckle_client,
|
||||
)
|
||||
assert status["status"] == automation_context.run_status
|
||||
@@ -205,7 +224,7 @@ def test_create_version_in_project_raises_error_for_same_model(
|
||||
) -> None:
|
||||
with pytest.raises(ValueError):
|
||||
automation_context.create_new_version_in_project(
|
||||
Base(), automation_context.automation_run_data.branch_name
|
||||
Base(), automation_context.automation_run_data.triggers[0].payload.model_id
|
||||
)
|
||||
|
||||
|
||||
@@ -220,8 +239,8 @@ def test_create_version_in_project(
|
||||
model, version = automation_context.create_new_version_in_project(
|
||||
root_object, "foobar"
|
||||
)
|
||||
isinstance(model, Model)
|
||||
isinstance(version, Version)
|
||||
assert isinstance(model, Model)
|
||||
assert isinstance(version, Version)
|
||||
|
||||
|
||||
@pytest.mark.skip(
|
||||
@@ -230,9 +249,11 @@ def test_create_version_in_project(
|
||||
def test_set_context_view(automation_context: AutomationContext) -> None:
|
||||
automation_context.set_context_view()
|
||||
|
||||
trigger = automation_context.automation_run_data.triggers[0].payload
|
||||
|
||||
assert automation_context.context_view is not None
|
||||
assert automation_context.context_view.endswith(
|
||||
f"models/{automation_context.automation_run_data.model_id}@{automation_context.automation_run_data.version_id}"
|
||||
f"models/{trigger.model_id}@{trigger.version_id}"
|
||||
)
|
||||
|
||||
automation_context.report_run_status()
|
||||
@@ -244,7 +265,7 @@ def test_set_context_view(automation_context: AutomationContext) -> None:
|
||||
|
||||
assert automation_context.context_view is not None
|
||||
assert automation_context.context_view.endswith(
|
||||
f"models/{automation_context.automation_run_data.model_id}@{automation_context.automation_run_data.version_id},{dummy_context}"
|
||||
f"models/{trigger.model_id}@{trigger.version_id},{dummy_context}"
|
||||
)
|
||||
automation_context.report_run_status()
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import requests
|
||||
|
||||
from specklepy.transports.server.retry_policy import setup_session
|
||||
|
||||
|
||||
def test_session_headers_without_auth():
|
||||
"""Check that Accept header is set and Authorization is not."""
|
||||
session = setup_session(None)
|
||||
assert isinstance(session, requests.Session)
|
||||
assert session.headers["Accept"] == "text/plain"
|
||||
assert "Authorization" not in session.headers
|
||||
|
||||
|
||||
def test_session_headers_with_auth():
|
||||
"""Check that Authorization header is properly added."""
|
||||
token = "abc123"
|
||||
session = setup_session(token)
|
||||
assert isinstance(session, requests.Session)
|
||||
assert session.headers["Authorization"] == f"Bearer {token}"
|
||||
assert session.headers["Accept"] == "text/plain"
|
||||
Reference in New Issue
Block a user