Compare commits

..

14 Commits

Author SHA1 Message Date
Dogukan Karatas c04a97780c update from main 2025-11-11 08:56:16 +01:00
Gergő Jedlicska 309c78da37 feat(automate_sdk): support version result reporting (#468)
Publish Python Package / test (push) Has been cancelled
Publish Python Package / Build and Publish Python Package (push) Has been cancelled
* feat(automate_sdk): support version result reporting

* fix(automate_tests): skip test, its not working in CI

* docs(automate_sdk): describe new args
2025-11-05 21:50:40 +01:00
Jedd Morgan ff812d5ad9 add 520 status to retry policy (#467) 2025-11-05 10:49:36 +00:00
Jedd Morgan 8edc0d5d78 feat(ifc)!: Display Value proxies for IFC importer (#466)
Publish Python Package / test (push) Has been cancelled
Publish Python Package / Build and Publish Python Package (push) Has been cancelled
* First pass

* Add applicationId for proxyInstance

* renamed revit instances

* renamed collection again

* again

* reverted main changes for manual testing

* small refactor of function def

* format matix
2025-10-31 13:54:46 +00:00
Jedd Morgan 78b3e99475 Remove unused server envars (#464) 2025-10-28 16:49:32 +00:00
Jedd Morgan ac9e081d49 feat(metrics): Introduce sync metrics (#462)
Publish Python Package / test (push) Has been cancelled
Publish Python Package / Build and Publish Python Package (push) Has been cancelled
* sync metrics tracking

* fix
2025-10-27 13:40:40 +01:00
Jedd Morgan 4bc95441b9 feat(serverTransport): Add urlib3 retry policy to requests.Session clients used by ServerTransport (#461)
Publish Python Package / test (push) Has been cancelled
Publish Python Package / Build and Publish Python Package (push) Has been cancelled
* Pre-commit on the public image

* change names

* first pass using urllib3 retry policy

* add some basic unit test

* correct the doc string
2025-10-15 21:43:45 +01:00
Jedd Morgan 0d74848b68 chore(ci): Pre-commit on the public image (#460)
* Pre-commit on the public image

* change names
2025-10-15 21:16:21 +01:00
Jedd Morgan 8a76006f9e feat(ci): Integration tests against internal image (#459)
* Integration tests against internal image

* fixed docker compose up

* auth

* add auth again!

* Update pr.yml
2025-10-14 11:37:15 +00:00
Dogukan Karatas c5cd69569e docs poc 2025-03-05 12:50:44 +01:00
Jedd Morgan e38249bc38 Moved docs one layer higher 2025-02-28 16:37:54 +00:00
Jedd Morgan 08fbf59c8a experiments 2025-02-28 16:21:43 +00:00
Jedd Morgan e9cdd3e900 round2 2025-02-28 15:34:59 +00:00
Jedd Morgan 20bb0449e8 first pass 2025-02-28 15:10:44 +00:00
66 changed files with 1005 additions and 166 deletions
+50 -10
View File
@@ -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
+118
View File
@@ -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
View File
@@ -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:
+4
View File
@@ -13,6 +13,10 @@ dependencies = [
"deprecated>=1.2.15",
"gql[requests,websockets]>=3.5.0,<4.0.0",
"httpx>=0.28.1",
"mkdocs>=1.6.1",
"mkdocs-material>=9.6.5",
"mkdocstrings>=0.28.1",
"mkdocstrings-python>=1.15.0",
"pydantic>=2.10.5",
"pydantic-settings>=2.7.1",
"ujson>=5.10.0",
+1
View File
@@ -0,0 +1 @@
::: specklepy.api.client.SpeckleClient
@@ -0,0 +1 @@
::: specklepy.objects.data_objects.DataObject
@@ -0,0 +1 @@
::: specklepy.objects.data_objects.QgisObject
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.IBlenderObject
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.ICurve
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.IDataObject
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.IDisplayValue
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.IBlenderObject
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.IHasArea
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.IHasUnits
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.IHasVolume
@@ -0,0 +1 @@
::: specklepy.objects.interfaces.IProperties
@@ -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.RenderMaterialProxy
+1
View File
@@ -0,0 +1 @@
::: specklepy.objects.base.Base
@@ -0,0 +1 @@
::: specklepy.objects.geometry.arc.Arc
@@ -0,0 +1 @@
::: specklepy.objects.geometry.box.Box
@@ -0,0 +1 @@
::: specklepy.objects.geometry.circle.Circle
@@ -0,0 +1 @@
::: specklepy.objects.geometry.control_point.ControlPoint
@@ -0,0 +1 @@
::: specklepy.objects.geometry.ellipse.Ellipse
@@ -0,0 +1 @@
::: specklepy.objects.geometry.line.Line
@@ -0,0 +1 @@
::: specklepy.objects.geometry.mesh.Mesh
@@ -0,0 +1 @@
::: specklepy.objects.geometry.plane.Plane
@@ -0,0 +1 @@
::: specklepy.objects.geometry.point.Point
@@ -0,0 +1 @@
::: specklepy.objects.geometry.point_cloud.PointCloud
@@ -0,0 +1 @@
::: specklepy.objects.geometry.polycurve.Polycurve
@@ -0,0 +1 @@
::: specklepy.objects.geometry.polyline.Polyline
@@ -0,0 +1 @@
::: specklepy.objects.geometry.spiral.Spiral
@@ -0,0 +1 @@
::: specklepy.objects.geometry.surface.Surface
@@ -0,0 +1 @@
::: specklepy.objects.geometry.vector.Vector
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

+29
View File
@@ -0,0 +1,29 @@
# Introduction
Welcome to the Specklepy Developer Docs - a single source of documentation on everything Specklepy! If you're looking for info on how to use Speckle, check our [user guide](https://speckle.guide/).
### Code Repository
The Python SDK can be found in our [repository](//github.com/specklesystems/specklepy), its readme contains instructions on how to build it.
### Installation
You can install it using pip
```
pip install specklepy
```
### Key Components
SpecklePy has three main parts:
1. a `SpeckleClient` which allows you to interact with the server API
2. `operations` and `transports` for sending and receiving large objects
3. a `Base` object and accompaniying serializer for creating and customizing your own Speckle objects
### Local Data Paths
It may be helpful to know where the local accounts and object cache dbs are stored. Depending on on your OS, you can find the dbs at:
- Windows: `APPDATA` or `<USER>\AppData\Roaming\Speckle`
- Linux: `$XDG_DATA_HOME` or by default `~/.local/share/Speckle`
- Mac: `~/.config/Speckle`
@@ -0,0 +1 @@
::: speckle_automate.automation_context.AutomationContext
+64
View File
@@ -0,0 +1,64 @@
site_name: Specklepy Docs
theme:
name: material
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:
- css/mkdocstrings.css
plugins:
- search
- mkdocstrings:
handlers:
python:
paths: [.]
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]
+39 -15
View File
@@ -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)
+5 -7
View File
@@ -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,
)
+12 -11
View File
@@ -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:
+4 -2
View File
@@ -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
+63 -12
View File
@@ -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,27 +12,38 @@ 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)
@@ -48,14 +59,14 @@ class ImportJob:
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)
id = step_element.id()
display_value = self.cached_display_values.get(id, [])
display_value = self._display_value_cache.get(id, [])
if display_value:
self.geometries_used += 1
@@ -127,12 +138,9 @@ class ImportJob:
shape = cast(TriangulationElement, iterator.get())
self.geometries_count += 1
id = cast(int, shape.id)
try:
display_value = geometry_to_speckle(
shape, self._render_material_manager
)
self.cached_display_values[id] = display_value
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}"
@@ -140,6 +148,37 @@ class ImportJob:
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:
@@ -147,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
+1 -1
View File
@@ -56,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
@@ -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
+60 -60
View File
@@ -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,43 @@ 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 not account:
return
if account:
METRICS_TRACKER.set_last_user(account.userInfo.email)
METRICS_TRACKER.set_last_server(account.serverInfo.url)
METRICS_TRACKER.set_last_user(account.userInfo.email)
METRICS_TRACKER.set_last_server(account.serverInfo.url)
return METRICS_TRACKER
class Singleton(type):
@@ -114,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 | None):
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 | None):
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()
+24
View File
@@ -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
+4
View File
@@ -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]
+39
View File
@@ -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
+33 -1
View File
@@ -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
+3 -3
View File
@@ -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()
@@ -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
+4 -15
View File
@@ -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:
@@ -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()
+20
View File
@@ -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"
Generated
+256
View File
@@ -1,4 +1,5 @@
version = 1
revision = 1
requires-python = ">=3.10.0, <4.0"
resolution-markers = [
"python_full_version >= '3.11'",
@@ -68,6 +69,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
]
[[package]]
name = "babel"
version = "2.17.0"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 },
]
[[package]]
name = "backoff"
version = "2.2.1"
@@ -86,6 +96,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 },
]
[[package]]
name = "backrefs"
version = "5.9"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267 },
{ url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072 },
{ url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947 },
{ url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843 },
{ url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762 },
{ url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265 },
]
[[package]]
name = "certifi"
version = "2024.12.14"
@@ -429,6 +453,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 },
]
[[package]]
name = "ghp-import"
version = "2.1.0"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "python-dateutil" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 },
]
[[package]]
name = "gql"
version = "3.5.0"
@@ -462,6 +498,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/dc/078bd6b304de790618ebb95e2aedaadb78f4527ac43a9ad8815f006636b6/graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a", size = 203189 },
]
[[package]]
name = "griffe"
version = "1.15.0"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "colorama" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705 },
]
[[package]]
name = "h11"
version = "0.14.0"
@@ -735,6 +783,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036 },
]
[[package]]
name = "markdown"
version = "3.10"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/7dd27d9d863b3376fcf23a5a13cb5d024aed1db46f963f1b5735ae43b3be/markdown-3.10.tar.gz", hash = "sha256:37062d4f2aa4b2b6b32aefb80faa300f82cc790cb949a35b8caede34f2b68c0e", size = 364931 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/81/54e3ce63502cd085a0c556652a4e1b919c45a446bd1e5300e10c44c8c521/markdown-3.10-py3-none-any.whl", hash = "sha256:b5b99d6951e2e4948d939255596523444c0e677c669700b1d17aa4a8a464cb7c", size = 107678 },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
@@ -814,6 +871,130 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "mergedeep"
version = "1.3.4"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 },
]
[[package]]
name = "mkdocs"
version = "1.6.1"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "click" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "ghp-import" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "markupsafe" },
{ name = "mergedeep" },
{ name = "mkdocs-get-deps" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "pyyaml" },
{ name = "pyyaml-env-tag" },
{ name = "watchdog" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 },
]
[[package]]
name = "mkdocs-autorefs"
version = "1.4.3"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "markdown" },
{ name = "markupsafe" },
{ name = "mkdocs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/51/fa/9124cd63d822e2bcbea1450ae68cdc3faf3655c69b455f3a7ed36ce6c628/mkdocs_autorefs-1.4.3.tar.gz", hash = "sha256:beee715b254455c4aa93b6ef3c67579c399ca092259cc41b7d9342573ff1fc75", size = 55425 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/4d/7123b6fa2278000688ebd338e2a06d16870aaf9eceae6ba047ea05f92df1/mkdocs_autorefs-1.4.3-py3-none-any.whl", hash = "sha256:469d85eb3114801d08e9cc55d102b3ba65917a869b893403b8987b601cf55dc9", size = 25034 },
]
[[package]]
name = "mkdocs-get-deps"
version = "0.2.0"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "mergedeep" },
{ name = "platformdirs" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 },
]
[[package]]
name = "mkdocs-material"
version = "9.6.23"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "babel" },
{ name = "backrefs" },
{ name = "colorama" },
{ name = "jinja2" },
{ name = "markdown" },
{ name = "mkdocs" },
{ name = "mkdocs-material-extensions" },
{ name = "paginate" },
{ name = "pygments" },
{ name = "pymdown-extensions" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/57/de/cc1d5139c2782b1a49e1ed1845b3298ed6076b9ba1c740ad7c952d8ffcf9/mkdocs_material-9.6.23.tar.gz", hash = "sha256:62ebc9cdbe90e1ae4f4e9b16a6aa5c69b93474c7b9e79ebc0b11b87f9f055e00", size = 4048130 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/df/bc583e857174b0dc6df67d555123533f09e7e1ac0f3fae7693fb6840c0a3/mkdocs_material-9.6.23-py3-none-any.whl", hash = "sha256:3bf3f1d82d269f3a14ed6897bfc3a844cc05e1dc38045386691b91d7e6945332", size = 9210689 },
]
[[package]]
name = "mkdocs-material-extensions"
version = "1.3.1"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 },
]
[[package]]
name = "mkdocstrings"
version = "0.30.1"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "jinja2" },
{ name = "markdown" },
{ name = "markupsafe" },
{ name = "mkdocs" },
{ name = "mkdocs-autorefs" },
{ name = "pymdown-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/33/2fa3243439f794e685d3e694590d28469a9b8ea733af4b48c250a3ffc9a0/mkdocstrings-0.30.1.tar.gz", hash = "sha256:84a007aae9b707fb0aebfc9da23db4b26fc9ab562eb56e335e9ec480cb19744f", size = 106350 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/2c/f0dc4e1ee7f618f5bff7e05898d20bf8b6e7fa612038f768bfa295f136a4/mkdocstrings-0.30.1-py3-none-any.whl", hash = "sha256:41bd71f284ca4d44a668816193e4025c950b002252081e387433656ae9a70a82", size = 36704 },
]
[[package]]
name = "mkdocstrings-python"
version = "1.19.0"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "griffe" },
{ name = "mkdocs-autorefs" },
{ name = "mkdocstrings" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/75/1c/3af8413919b0839b96a78f60e8bd0dfd26c844d3717eeb77f80b43f5be1c/mkdocstrings_python-1.19.0.tar.gz", hash = "sha256:917aac66cf121243c11db5b89f66b0ded6c53ec0de5318ff5e22424eb2f2e57c", size = 204010 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/5c/2597cef67b6947b15c47f8dba967a0baf19fbdfdc86f6e4a8ba7af8b581a/mkdocstrings_python-1.19.0-py3-none-any.whl", hash = "sha256:395c1032af8f005234170575cc0c5d4d20980846623b623b35594281be4a3059", size = 143417 },
]
[[package]]
name = "more-itertools"
version = "10.6.0"
@@ -1039,6 +1220,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
]
[[package]]
name = "paginate"
version = "0.5.7"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 },
]
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -1308,6 +1498,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "pymdown-extensions"
version = "10.16.1"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178 },
]
[[package]]
name = "pytest"
version = "8.3.4"
@@ -1436,6 +1639,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
]
[[package]]
name = "pyyaml-env-tag"
version = "1.1"
source = { registry = "https://pypi.org/simple/" }
dependencies = [
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722 },
]
[[package]]
name = "questionary"
version = "2.1.0"
@@ -1638,6 +1853,10 @@ dependencies = [
{ name = "deprecated" },
{ name = "gql", extra = ["requests", "websockets"] },
{ name = "httpx" },
{ name = "mkdocs" },
{ name = "mkdocs-material" },
{ name = "mkdocstrings" },
{ name = "mkdocstrings-python" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "ujson" },
@@ -1673,10 +1892,15 @@ requires-dist = [
{ name = "gql", extras = ["requests", "websockets"], specifier = ">=3.5.0,<4.0.0" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "ifcopenshell", marker = "extra == 'speckleifc'", specifier = ">=0.8.3.post2" },
{ name = "mkdocs", specifier = ">=1.6.1" },
{ name = "mkdocs-material", specifier = ">=9.6.5" },
{ name = "mkdocstrings", specifier = ">=0.28.1" },
{ name = "mkdocstrings-python", specifier = ">=1.15.0" },
{ name = "pydantic", specifier = ">=2.10.5" },
{ name = "pydantic-settings", specifier = ">=2.7.1" },
{ name = "ujson", specifier = ">=5.10.0" },
]
provides-extras = ["speckleifc"]
[package.metadata.requires-dev]
dev = [
@@ -1923,6 +2147,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 },
]
[[package]]
name = "watchdog"
version = "6.0.0"
source = { registry = "https://pypi.org/simple/" }
sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390 },
{ url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389 },
{ url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020 },
{ url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 },
{ url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 },
{ url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 },
{ url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 },
{ url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 },
{ url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 },
{ url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 },
{ url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 },
{ url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 },
{ url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902 },
{ url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380 },
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 },
{ url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 },
{ url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 },
{ url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 },
{ url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 },
{ url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 },
{ url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 },
{ url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 },
{ url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 },
{ url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 },
]
[[package]]
name = "wcwidth"
version = "0.2.13"