Compare commits

..

2 Commits

Author SHA1 Message Date
Jedd Morgan 13bb7d56c8 Fixed unused imports 2024-02-15 13:38:16 +00:00
Jedd Morgan b1be7780df feat: CNX-8705 fe2 ur ls in blender (#182)
* Increased default branch get to 100 limit, and added as mesh conversion for text + surfaces + metaball

* poetry lock

* Upgraded typing module

* FE2 URL support

* Raised exceptions now display to user
2024-02-13 13:35:01 +00:00
86 changed files with 6086 additions and 7747 deletions
+302 -12
View File
@@ -1,17 +1,307 @@
version: 2.1
# Define the jobs we want to run for this project
jobs:
build:
docker:
- image: cimg/base:2023.03
steps:
- run: echo "so long and thanks for all the fish"
orbs:
win: circleci/windows@5.0.0
jobs:
package-connector:
docker:
- image: cimg/python:3.11.0
steps:
- checkout
- run:
name: Setup SEMVER value
command: |
SEMVER=$(if [ "${CIRCLE_TAG}" ]; then echo $CIRCLE_TAG; else echo "0.0.0"; fi;)
echo $SEMVER > ./SEMVER
python3 patch_version.py $SEMVER
- run:
name: install dependencies
command: poetry install --only main
- run:
name: export package dependencies
command: ./export_dependencies.sh
- persist_to_workspace:
root: ./
paths:
- bpy_speckle
- patch_installer.py
- SEMVER
build-connector-zip:
docker:
- image: cimg/python:3.11.0
steps:
- attach_workspace:
at: ./
- run: &restore_semver
name: Restore Semver
command: SEMVER=$(cat ./SEMVER) && echo $SEMVER
- run:
name: Package to Zip
command: zip -r bpy_speckle.zip bpy_speckle
- persist_to_workspace:
root: ./
paths:
- bpy_speckle.zip
get-ci-tools: # Clones our ci tools and persists them to the workspace
docker:
- image: cimg/base:2021.01
steps:
- add_ssh_keys:
fingerprints:
- "77:64:03:93:c5:f3:1d:a6:fd:bd:fb:d1:05:56:ca:e9"
- run:
name: I know Github as a host
command: |
mkdir ~/.ssh
touch ~/.ssh/known_hosts
ssh-keyscan github.com >> ~/.ssh/known_hosts
- run:
name: Clone
command: git clone git@github.com:specklesystems/speckle-sharp-ci-tools.git speckle-sharp-ci-tools
- run:
command: cd speckle-sharp-ci-tools
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools
build-installer-win:
executor:
name: win/default
shell: cmd.exe
steps:
- attach_workspace:
at: ./
- run:
name: Patch installer
shell: powershell.exe
command: python patch_installer.py (Get-Content -Raw SEMVER)
- run:
name: Create Innosetup signing cert
shell: powershell.exe
command: |
echo $env:PFX_B64 > "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt"
certutil -decode "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.txt" "speckle-sharp-ci-tools\SignTool\AEC Systems Ltd.pfx"
- run:
name: Installer
shell: cmd.exe #does not work in powershell
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/blender-*.exe
build-installer-mac:
macos:
xcode: 12.5.1
parameters:
runtime:
type: string
slug:
type: string
installer_path:
type: string
default: speckle-sharp-ci-tools/Mac/SpeckleBlenderInstall
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: Install mono
command: |
HOMEBREW_NO_AUTO_UPDATE=1 brew install mono
# Compress build files
- run:
name: Install dotnet
command: curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin
- run: *restore_semver
- run:
name: Copy connector files to installer
command: |
mkdir -p <<parameters.installer_path >>/.installationFiles/
cp bpy_speckle.zip << parameters.installer_path >>/.installationFiles
- run:
name: Build Mac installer
command: ~/.dotnet/dotnet publish << parameters.installer_path >>/SpeckleBlenderInstall.sln -r << parameters.runtime >> -c Release
- run:
name: Zip installer
command: |
SEMVER=$(cat ./SEMVER)
echo $SEMVER
mkdir -p speckle-sharp-ci-tools/Installers/blender
(cd <<parameters.installer_path>>/bin/Release/net6.0/<< parameters.runtime >>/publish/ && zip -r - ./) > << parameters.slug >>-${SEMVER}.zip
cp << parameters.slug >>-${SEMVER}.zip speckle-sharp-ci-tools/Installers/blender/
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
build-installer-manual:
docker:
- image: cimg/base:2021.01
parameters:
slug:
type: string
default: bpy_speckle
steps:
- attach_workspace:
at: ./
- run: *restore_semver
- run:
name: Copy zip with semver
command: |
SEMVER=$(cat ./SEMVER)
mkdir -p speckle-sharp-ci-tools/Installers/blender
cp bpy_speckle.zip speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>-${SEMVER}.zip
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/<< parameters.slug >>*.zip
deploy-connector:
docker:
- image: mcr.microsoft.com/dotnet/sdk:6.0
parameters:
file_slug:
type: string
os:
type: string
extension:
type: string
arch:
type: string
default: Any
steps:
- checkout
- attach_workspace:
at: ./
- run:
name: Install Manager Feed CLI
command: dotnet tool install --global Speckle.Manager.Feed
- run: *restore_semver
- run:
name: Upload new version
# this is where the installer gets the semver baked into the file name
command: |
SEMVER=$(cat ./SEMVER)
echo $SEMVER
/root/.dotnet/tools/Speckle.Manager.Feed deploy \
-s blender \
-v ${SEMVER} \
-u https://releases.speckle.dev/installers/blender/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >> \
-o << parameters.os >> \
-a << parameters.arch >> \
-f speckle-sharp-ci-tools/Installers/blender/<< parameters.file_slug >>-${SEMVER}.<< parameters.extension >>
# Orchestrate our job run sequence
workflows:
build_and_test:
when:
false
build: # build the installers, but don't persist to workspace for deployment
jobs:
- build
- package-connector:
filters: &build_filters
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
- build-connector-zip:
requires:
- package-connector
filters: *build_filters
- get-ci-tools:
filters: *build_filters
- build-installer-win:
context: innosetup
name: Windows Installer Build
requires:
- package-connector
- get-ci-tools
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-windows
file_slug: blender
os: WIN
arch: Any
extension: exe
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: &deploy_filters
branches:
ignore: /.*/
tags:
only: /([0-9]+)\.([0-9]+)\.([0-9]+)(?:-\w+)?(?:\.[0-9]+)?/
- build-installer-mac:
name: Mac ARM Build
slug: blender-mac-arm
runtime: osx-arm64
requires:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-arm
file_slug: blender-mac-arm
os: OSX
arch: Arm
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
- build-installer-mac:
name: Mac Intel Build
slug: blender-mac-intel
runtime: osx-x64
requires:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-mac-intel
file_slug: blender-mac-intel
os: OSX
arch: Intel
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
- build-installer-manual:
name: Manual Installer Build
requires:
- get-ci-tools
- build-connector-zip
filters: *build_filters
- deploy-connector:
context: do-spaces-speckle-releases
name: deploy-manual
file_slug: bpy_speckle
os: Any
arch: Any
extension: zip
requires:
- Manual Installer Build
- Windows Installer Build
- Mac Intel Build
- Mac ARM Build
filters: *deploy_filters
+12
View File
@@ -0,0 +1,12 @@
name: Update issue Status
on:
issues:
types: [closed]
jobs:
update_issue:
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
+12
View File
@@ -0,0 +1,12 @@
name: Move new issues into Project
on:
issues:
types: [opened]
jobs:
track_issue:
uses: specklesystems/github-actions/.github/workflows/project-add-issue.yml@main
secrets: inherit
with:
issue-id: ${{ github.event.issue.node_id }}
-31
View File
@@ -1,31 +0,0 @@
name: "PR workflow"
on:
pull_request:
branches:
- "v3-dev"
jobs:
build:
name: Pre-commit Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v5
with:
python-version: "3.11"
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install the project
run: uv sync --all-extras --dev
# - uses: actions/cache@v3
# with:
# path: ~/.cache/pre-commit/
# key: ${{ hashFiles('.pre-commit-config.yaml') }}
# - name: Run pre-commit
# run: uv run pre-commit run --all-files
- name: Minimize uv cache
run: uv cache prune --ci
-96
View File
@@ -1,96 +0,0 @@
name: "Release workflow"
on:
push:
branches: ["main", "installer-test/**"]
tags: ["v3.*.*"]
jobs:
build:
name: Build Zip
runs-on: ubuntu-latest
env:
ZIP_NAME: "blender.zip"
SEMVER: null
FILE_VERSION: null
outputs:
semver: ${{ steps.set-version.outputs.semver }}
fileVersion: ${{ steps.set-version.outputs.fileVersion }}
steps:
- uses: actions/checkout@v4
- name: 🐍 Install uv and set the python version
uses: astral-sh/setup-uv@v5
with:
python-version: "3.11"
enable-cache: true
cache-dependency-glob: "uv.lock"
- id: set-version
name: Set version to output
run: |
TAG=${{ github.ref_name }}
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
TAG="v3.0.99.${{ github.run_number }}"
fi
SEMVER="${TAG#v}"
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
echo $SEMVER
echo $FILE_VERSION
- name: ✏ Patch Version
run: python patch_version.py ${{ steps.set-version.outputs.fileVersion }}
- name: 🔄 UV Sync
run: uv sync --all-extras --dev
- name: 📄 Export Package Dependencies
run: ./export_dependencies.sh
- name: 🗃 Zip Package
run: zip -r ${{env.ZIP_NAME}} bpy_speckle
- name: ⬆️ Upload artifacts
uses: actions/upload-artifact@v4
with:
name: output-${{ steps.set-version.outputs.semver }}
path: ${{env.ZIP_NAME}}
if-no-files-found: error
retention-days: 1
compression-level: 0 # no compression
- name: 💾 Minimize uv cache
run: uv cache prune --ci
deploy-installers:
runs-on: ubuntu-latest
needs: build
env:
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
steps:
- name: 🔫 Trigger Build Installer(s)
uses: the-actions-org/workflow-dispatch@v4.0.0
with:
workflow: Build Installers
repo: specklesystems/connector-installers
token: ${{ secrets.CONNECTORS_GH_TOKEN }}
inputs: '{
"run_id": "${{ github.run_id }}",
"semver": "${{ needs.build.outputs.semver }}",
"file_version": "${{ needs.build.outputs.fileVersion }}",
"repo": "${{ github.repository }}",
"is_public_release": ${{ env.IS_PUBLIC_RELEASE }}
}'
ref: main
wait-for-completion: true
wait-for-completion-interval: 10s
wait-for-completion-timeout: 10m
display-workflow-run-url: true
display-workflow-run-url-interval: 10s
- uses: geekyeggo/delete-artifact@v5
with:
name: output-*
+1 -2
View File
@@ -13,5 +13,4 @@ Installers/
modules/
.tool-versions
requirements.txt
SEMVER
dui3/
SEMVER
+24 -8
View File
@@ -45,29 +45,45 @@ Give Speckle a try in no time by:
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) for more information.
## Installation
We officially support Blender 4.2 and newer, on Windows.
Currently, we are supporting all Blender 3.X versions on Windows and Mac.
We have experimental support for Blender 4.0 and greater.
Please follow our installation instructions on our [connector docs](https://speckle.guide/user/blender.html#installation)
## Usage
Once enabled in `Preferences -> Addons`,
The Speckle connector UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
- Available user accounts are automatically detected and made available.
- Select the account from the dropdown list in the `Accounts` panel. This will populate the `Projects` list with available projects for the selected user account.
- Select a model and version from the dropdown menus.
- Click on `Load` to download and convert the objects from the selected model version. The objects will be linked into a Blender Collection.
- Click on `Open Model in Web` to view the model in your web browser.
- Available user accounts are automatically detected and made available. To add user accounts use **Speckle Manager**.
- Select the user from the dropdown list in the `Users` panel. This will populate the `Streams` list with available streams for the selected user.
- Select a branch and commit from the dropdown menus.
- Click on `Receive` to download the objects from the selected stream, branch, and commit. The stream objects will be loaded into a Blender Collection, named `<STREAM_NAME> [ <STREAM_BRANCH> @ <BRANCH_COMMIT> ]`. <!-- You can filter the stream by entering a query into the `Filter` field (i.e. `properties.weight>10` or `type="Mesh"`). -->
- Click on `Open Stream in Web` to view the stream in your web browser.
## Supported Elements
The Blender Connector is still a work in progress and, as such, data sent from the Blender connector is a highly lossy exchange. Our connectors are ever evolving to facilitate more and more Speckle usecases. We welcome feedback, requests, edge cases, and contributions!
The full matrix of supported Blender and Speckle types [can be found here](https://speckle.guide/user/support-tables.html#blender)
## Additional Features
- Speckle properties will be imported as custom properties on Blender objects. Nested dictionaries are expanded to individual properties by flattening their key hierarchy. I.e. `propA:{'propB': {'propC':10, 'propD':'foobar'}}` is flattened to `propA.propB.propC = 10` and `propA.propB.propD = "foobar"`.
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
- Receiving vertex colors are supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- Receive/Send scripts. Allow injecting a custom python function to the receive/send process to automate any blender operations
## Dependency Installation and Compatibility with Other Blender Addons
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows.
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows and `~/.config/Speckle/connector_installations` on Mac.
This is done through our [`installer.py`](https://github.com/specklesystems/speckle-blender/blob/main/bpy_speckle/installer.py). Through pip, we install the correct version of each dependency for your blender python version, host OS, and system architecture.
As such, an internet connection is required for first launch of the connector.
+87 -191
View File
@@ -1,217 +1,113 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ruff: noqa
import bpy
from bpy.types import WindowManager
from .connector.ui import icons
# Ensure dependencies
from .installer import ensure_dependencies
from bpy_speckle.installer import ensure_dependencies
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
from specklepy.logging import metrics
from bpy_speckle.ui import *
from bpy_speckle.properties import *
from bpy_speckle.operators import *
from bpy_speckle.callbacks import *
from bpy.app.handlers import persistent
bl_info = {
"name": "Speckle Connector",
"author": "Speckle",
"version": (3, 999, 999),
"blender": (4, 2, 0),
"name": "SpeckleBlender 2.0",
"author": "Speckle Systems",
"version": (0, 2, 0),
"blender": (2, 92, 0),
"location": "3d viewport toolbar (N), under the Speckle tab.",
"description": "Publish models to and load models from other AEC apps.",
"wiki_url": "https://speckle.systems/connectors/blender",
"description": "The Speckle Connector using specklepy 2.0!",
"warning": "This add-on is WIP and should be used with caution",
"wiki_url": "https://github.com/specklesystems/speckle-blender",
"category": "Scene",
}
# UI
from .connector.ui.main_panel import SPECKLE_PT_main_panel
from .connector.utils.account_manager import speckle_workspace
from .connector.ui.project_selection_dialog import (
SPECKLE_OT_project_selection_dialog,
SPECKLE_UL_projects_list,
)
from .connector.ui.model_selection_dialog import (
SPECKLE_OT_model_selection_dialog,
SPECKLE_UL_models_list,
)
from .connector.ui.version_selection_dialog import (
SPECKLE_OT_version_selection_dialog,
SPECKLE_UL_versions_list,
)
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog
from .connector.utils.property_groups import (
speckle_project,
speckle_model,
speckle_version,
speckle_object,
speckle_collection,
speckle_model_card,
)
# Operators
from .connector.blender_operators.publish_button import SPECKLE_OT_publish
from .connector.blender_operators.load_button import SPECKLE_OT_load
from .connector.blender_operators.model_card_settings import (
SPECKLE_OT_model_card_settings,
SPECKLE_OT_view_in_browser,
SPECKLE_OT_view_model_versions,
SPECKLE_OT_delete_model_card,
)
from .connector.blender_operators.select_objects import SPECKLE_OT_select_objects
from .connector.blender_operators.add_account_button import SPECKLE_OT_add_account
from .connector.blender_operators.model_card_load_button import (
SPECKLE_OT_load_model_card,
)
from .connector.blender_operators.model_card_publish_button import (
SPECKLE_OT_publish_model_card,
)
from .connector.blender_operators.add_project_by_url import (
SPECKLE_OT_add_project_by_url,
)
"""
Import SpeckleBlender classes
"""
from .connector.blender_operators.create_project import SPECKLE_OT_create_project
from .connector.blender_operators.create_model import SPECKLE_OT_create_model
from .connector.utils.account_manager import (
speckle_account,
get_default_account_id,
_client_cache,
)
"""
Add load handler to initialize Speckle when
loading a Blender file
"""
# States
from .connector.states.speckle_state import (
register as register_speckle_state,
unregister as unregister_speckle_state,
)
@persistent
def load_handler(dummy):
pass
# Calling users_load is an expensive operation, one that force users to wait a good 10s every time blender loads.
# Until we can do this non-blocking, we will make the user hit the refresh button each time.
#bpy.ops.speckle.users_load()
# Instead, we shall just reset the user selection to an uninitiailised state
bpy.ops.speckle.users_reset()
"""
Permanent handle on callbacks
"""
callbacks = {}
"""
Add Speckle classes for registering
"""
speckle_classes = []
speckle_classes.extend(operator_classes)
speckle_classes.extend(property_classes)
speckle_classes.extend(ui_classes)
from .connector.ui.workspace_selection_dialog import (
SPECKLE_OT_workspace_selection_dialog,
SPECKLE_UL_workspaces_list,
)
# Utils
from .connector.ui.account_selection_dialog import (
SPECKLE_OT_account_selection_dialog,
SPECKLE_UL_accounts_list,
)
def invoke_window_manager_properties():
# Accounts
WindowManager.speckle_accounts = bpy.props.CollectionProperty(type=speckle_account)
WindowManager.selected_account_id = bpy.props.StringProperty()
# Workspaces
WindowManager.speckle_workspaces = bpy.props.CollectionProperty(
type=speckle_workspace
)
WindowManager.selected_workspace = bpy.props.PointerProperty(type=speckle_workspace)
WindowManager.can_create_project_in_workspace = bpy.props.BoolProperty()
# Projects
WindowManager.speckle_projects = bpy.props.CollectionProperty(type=speckle_project)
WindowManager.selected_project_id = bpy.props.StringProperty()
WindowManager.selected_project_name = bpy.props.StringProperty()
# Models
WindowManager.speckle_models = bpy.props.CollectionProperty(type=speckle_model)
WindowManager.selected_model_id = bpy.props.StringProperty()
WindowManager.selected_model_name = bpy.props.StringProperty()
# Versions
WindowManager.speckle_versions = bpy.props.CollectionProperty(type=speckle_version)
WindowManager.selected_version_id = bpy.props.StringProperty()
WindowManager.selected_version_load_option = bpy.props.StringProperty()
# Send / Publish buttons
WindowManager.ui_mode = bpy.props.EnumProperty( # type: ignore
name="UI Mode",
description="Publish or Load a model",
items=[
("PUBLISH", "Publish", "Publish a model to Speckle", "EXPORT", 0),
("LOAD", "Load", "Load a model from Speckle", "IMPORT", 1),
],
default="PUBLISH",
)
# Objects
WindowManager.speckle_objects = bpy.props.CollectionProperty(type=speckle_object)
# Classes to load
classes = (
SPECKLE_PT_main_panel,
SPECKLE_OT_publish,
SPECKLE_OT_load,
SPECKLE_OT_project_selection_dialog,
speckle_project,
SPECKLE_UL_projects_list,
speckle_workspace,
SPECKLE_OT_model_selection_dialog,
speckle_model,
SPECKLE_UL_models_list,
SPECKLE_OT_version_selection_dialog,
speckle_version,
SPECKLE_UL_versions_list,
SPECKLE_OT_selection_filter_dialog,
speckle_object,
speckle_collection,
speckle_model_card,
SPECKLE_OT_model_card_settings,
SPECKLE_OT_view_in_browser,
SPECKLE_OT_view_model_versions,
SPECKLE_OT_delete_model_card,
SPECKLE_OT_select_objects,
SPECKLE_OT_add_account,
SPECKLE_OT_load_model_card,
SPECKLE_OT_publish_model_card,
SPECKLE_OT_add_project_by_url,
SPECKLE_OT_create_project,
SPECKLE_OT_create_model,
speckle_account,
SPECKLE_UL_workspaces_list,
SPECKLE_OT_workspace_selection_dialog,
SPECKLE_OT_account_selection_dialog,
SPECKLE_UL_accounts_list,
)
# Register and Unregister
def register():
icons.load_icons()
from bpy.utils import register_class
for cls in classes:
bpy.utils.register_class(cls)
register_speckle_state() # Register SpeckleState
for cls in speckle_classes:
register_class(cls)
invoke_window_manager_properties()
metrics.set_host_app("blender", f"blender {bpy.app.version_string}")
# Pre-warm client cache for default account
try:
default_account_id = get_default_account_id()
if default_account_id:
print(
f"[Speckle] Pre-warming client for default account: {default_account_id}"
)
_client_cache.get_client(default_account_id)
print(
f"[Speckle] Client pre-warming complete for account: {default_account_id}"
)
except Exception as e:
print(f"[Speckle] Failed to pre-warm client: {e}")
"""
Register all new properties
"""
bpy.types.Scene.speckle = bpy.props.PointerProperty(type=SpeckleSceneSettings)
bpy.types.Collection.speckle = bpy.props.PointerProperty(
type=SpeckleCollectionSettings
)
bpy.types.Object.speckle = bpy.props.PointerProperty(type=SpeckleObjectSettings)
"""
Add callbacks
"""
# Callback for displaying the current user account on top of the 3d view
# callbacks['view3d_status'] = ((
# bpy.types.SpaceView3D.draw_handler_remove, # Function pointer for removal
# bpy.types.SpaceView3D.draw_handler_add(draw_speckle_info, (None, None), 'WINDOW', 'POST_PIXEL'), # Add handler
# 'WINDOW' # Callback space for removal
# ))
bpy.app.handlers.load_post.append(load_handler)
def unregister():
icons.unload_icons()
unregister_speckle_state() # Unregister SpeckleState
_client_cache.clear()
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(load_handler)
"""
Remove callbacks
"""
for cb in callbacks.values():
cb[0](cb[1], cb[2])
from bpy.utils import unregister_class
for cls in reversed(speckle_classes):
unregister_class(cls)
# Run the register function when the script is executed
if __name__ == "__main__":
register()
@@ -0,0 +1,120 @@
from typing import Dict, Optional, Tuple, Union
import bpy
from bpy.types import Object, Collection, ID
from specklepy.objects.base import Base
from bpy_speckle.functions import _report
from specklepy.objects.graph_traversal.commit_object_builder import CommitObjectBuilder, ROOT
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
from attrs import define
ELEMENTS = "elements"
def _id(native_object: ID) -> str:
#NOTE: to avoid naming collisions, we prefix collections and objects differently
return f"{type(native_object).__name__}:{native_object.name_full}"
def _try_id(native_object: Optional[Union[Collection, Object]]) -> Optional[str]:
return _id(native_object) if native_object else None
def convert_collection_to_speckle(col: Collection) -> SCollection:
converted_collection = SCollection(name = col.name_full, collectionType = "Blender Collection", elements = [])
converted_collection.applicationId = _id(col)
color_tag = col.color_tag
if color_tag and color_tag != "NONE":
converted_collection["colorTag"] = col.color_tag
return converted_collection
@define(slots=True)
class BlenderCommitObjectBuilder(CommitObjectBuilder[Object]):
_collections: Dict[str, SCollection]
def __init__(self) -> None:
super().__init__()
self._collections = {}
def include_object(self, conversion_result: Base, native_object: Object) -> None:
# Set the Child -> Parent relationships
parent = native_object.parent
parent_collections = native_object.users_collection
parent_collection = parent_collections[0] if len(parent_collections) > 0 else None #NOTE: we don't support objects appearing in more than one collection, for now, we will just take the zeroth one
app_id = _id(native_object)
conversion_result.applicationId = app_id
self.converted[app_id] = conversion_result
# in order or priority, direct parent, direct parent collection, root
self.set_relationship(app_id, (_try_id(parent), ELEMENTS), (_try_id(parent_collection), ELEMENTS), (ROOT, ELEMENTS))
# if parent_collection:
# self._include_collection(parent_collection)
def ensure_collection(self, col: Collection) -> SCollection:
id = _id(col)
if id in self._collections:
return self._collections[id] # collection already converted!
# Set the Parent -> Children relationships
for c in col.children:
#NOTE: There's no falling back to the grandparent, if the direct parent collection wasn't converted, then we we fallback to the root
self.set_relationship(_id(c), (id, ELEMENTS), (ROOT, ELEMENTS))
# Set Child -> Parent relationship
# parent = self.find_collection_parent(col)
# self.set_relationship(id, (_try_builder_id(parent), ELEMENTS), (ROOT, ELEMENTS))
converted_collection = convert_collection_to_speckle(col)
self.converted[id] = converted_collection
self._collections[id] = converted_collection
return converted_collection
def build_commit_object(self, root_commit_object: Base) -> None:
assert(root_commit_object.applicationId in self.converted)
# Create all collections
root_col = self.ensure_collection(bpy.context.scene.collection)
root_col.collectionType = "Scene Collection"
for col in bpy.context.scene.collection.children_recursive:
self.ensure_collection(col)
objects_to_build = set(self.converted.values())
objects_to_build.remove(root_commit_object)
self.apply_relationships(objects_to_build, root_commit_object)
assert(isinstance(root_commit_object, SCollection))
# Kill unused collections
def should_remove_unuseful_collection(col: SCollection) -> bool: #TODO: this maybe could be optimised
elements = col.elements
if not elements: return True
should_remove_this_col = True
i = 0
while i < len(elements):
c = elements[i]
if not isinstance(c, SCollection):
# col has objects (c)
should_remove_this_col = False
i += 1
continue
if should_remove_unuseful_collection(c):
# c is not useful, kill it
del elements[i]
else:
# col has a child (c) with objects
should_remove_this_col = False
i += 1
continue
return should_remove_this_col
if should_remove_unuseful_collection(root_commit_object):
_report("WARNING: Only empty collections have been converted!") #TODO: consider raising exception here, to halt the send operation
-66
View File
@@ -1,66 +0,0 @@
schema_version = "1.0.0"
# Example of manifest file for a Blender extension
# Change the values according to your extension
id = "speckle_blender_addon"
version = "3.0.0"
name = "Speckle Connector"
tagline = "Load models from other AEC apps into Blender with Speckle."
maintainer = "Speckle"
# Supported types: "add-on", "theme"
type = "add-on"
# Optional link to documentation, support, source files, etc
website = "https://speckle.systems/connectors/blender"
# Optional list defined by Blender and server, see:
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
tags = ["Scene"]
blender_version_min = "4.2.0"
# # Optional: Blender version that the extension does not support, earlier versions are supported.
# # This can be omitted and defined later on the extensions platform if an issue is found.
# blender_version_max = "5.1.0"
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
license = ["SPDX:Apache-2.0"]
# Optional: required by some licenses.
copyright = ["2022-2025 AEC SYSTEMS LTD"]
# Optional list of supported platforms. If omitted, the extension will be available in all operating systems.
# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
# Other supported platforms: "windows-arm64", "macos-x64"
# Optional: bundle 3rd party Python modules.
# https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
# wheels = [
# "./wheels/hexdump-3.3-py3-none-any.whl",
# "./wheels/jsmin-3.0.1-py3-none-any.whl",
# ]
# Optional: add-ons can list which resources they will require:
# * files (for access of any filesystem operations)
# * network (for internet access)
# * clipboard (to read and/or write the system clipboard)
# * camera (to capture photos and videos)
# * microphone (to capture audio)
# permissions = ["files", "network", "clipboard"]
# If using network, remember to also check `bpy.app.online_access`
# https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
#
# For each permission it is important to also specify the reason why it is required.
# Keep this a single short sentence without a period (.) at the end.
# For longer explanations use the documentation or detail page.
#
[permissions]
network = "Speckle Server comms, and PyPI for dependency management"
files = "Data caching, Account Management, Python dependency management"
clipboard = "Copy and paste URLs and Names (UI)"
# Optional: build settings.
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
[build]
paths_exclude_pattern = ["__pycache__/", "/.vscode", "*.code-workspace"]
+2
View File
@@ -0,0 +1,2 @@
from .on_mesh_edit import scb_on_mesh_edit
from .draw_speckle_info import draw_speckle_info
@@ -0,0 +1,23 @@
"""
Drawing callback to display active Speckle user
"""
import blf
import bpy
def draw_speckle_info(self, context):
"""
Draw active user info on the 3d viewport
"""
scn = bpy.context.scene
if len(scn.speckle.users) > 0:
user = scn.speckle.users[int(scn.speckle.active_user)]
dpi = bpy.context.preferences.system.dpi
blf.position(0, 100, 50, 0)
blf.size(0, 20, dpi)
blf.draw(0, "Active Speckle user: {} ({})".format(user.name, user.email))
blf.position(0, 100, 20, 0)
blf.size(0, 16, dpi)
blf.draw(0, "Server: {}".format(user.server))
+13
View File
@@ -0,0 +1,13 @@
import bpy
from bpy.app.handlers import persistent
@persistent
def scb_on_mesh_edit(context):
"""
DEPRECATED
Do something whenever a mesh is updated
"""
edit_obj = bpy.context.edit_object
if edit_obj is not None and edit_obj.is_updated_data is True:
print("Mesh edited: {}".format(edit_obj))
+7
View File
@@ -0,0 +1,7 @@
"""
Permanent handle on all user clients
"""
from specklepy.core.api.client import SpeckleClient
speckle_clients: list[SpeckleClient] = []
@@ -1,9 +0,0 @@
from ..blender_operators.load_button import SPECKLE_OT_load # noqa: F401
from .model_card_load_button import SPECKLE_OT_load_model_card # noqa: F401
from ..blender_operators.publish_button import SPECKLE_OT_publish # noqa: F401
from ..blender_operators.model_card_settings import (
SPECKLE_OT_model_card_settings, # noqa: F401
SPECKLE_OT_view_in_browser, # noqa: F401
SPECKLE_OT_view_model_versions, # noqa: F401
SPECKLE_OT_delete_model_card, # noqa: F401
)
@@ -1,38 +0,0 @@
import bpy
import webbrowser
from bpy.types import Event, Context
class SPECKLE_OT_add_account(bpy.types.Operator):
"""Operator for adding a new Speckle account."""
bl_idname = "speckle.add_account"
bl_label = "Add New Account"
bl_description = "Add a new account"
server_url: bpy.props.StringProperty( # type: ignore
name="Server URL",
description="Speckle server URL to connect to",
default="https://app.speckle.systems",
)
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context):
layout = self.layout
# Server URL textbox
layout.prop(self, "server_url", text="Server URL")
def execute(self, context: Context) -> set[str]:
# Logic to handle sign in
api_url = "http://localhost:29364"
url = f"{api_url}/auth/add-account?serverUrl={self.server_url}"
webbrowser.open(url)
self.report({"INFO"}, f"Adding account from {self.server_url}: {url}")
# Force redraw
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
@@ -1,75 +0,0 @@
import bpy
from bpy.types import Context, Event, UILayout
from ..utils.account_manager import (
get_model_details_by_wrapper,
get_project_from_url,
can_load,
)
class SPECKLE_OT_add_project_by_url(bpy.types.Operator):
"""
operator for adding a Speckle project by URL
"""
bl_idname = "speckle.add_project_by_url"
bl_label = "Add Project by URL"
bl_description = "Add a project from a URL"
url: bpy.props.StringProperty( # type: ignore
name="Project URL", description="Enter the Speckle project URL", default=""
)
def execute(self, context: Context) -> set[str]:
self.report({"INFO"}, f"Adding project from URL: {self.url}")
wm = context.window_manager
# Get project from URL
wrapper, client, project, error_message = get_project_from_url(self.url)
if error_message:
self.report({"ERROR"}, error_message)
return {"CANCELLED"}
# Get model details from the wrapper
(
account_id,
project_id,
project_name,
model_id,
model_name,
version_id,
load_option,
) = get_model_details_by_wrapper(wrapper)
# Check permissions
can_load_permission, permission_error = can_load(client, project)
if not can_load_permission:
self.report({"ERROR"}, permission_error)
return {"CANCELLED"}
# Update the window manager with the selected project/model/version
wm.selected_account_id = account_id
if project_id:
wm.selected_project_id = project_id
wm.selected_project_name = project_name
if model_id:
wm.selected_model_id = model_id
wm.selected_model_name = model_name
if version_id:
wm.selected_version_id = version_id
wm.selected_version_id = version_id
wm.selected_version_load_option = load_option
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.prop(self, "url", text="")
@@ -1,68 +0,0 @@
import bpy
from bpy.types import Context, Event, UILayout
from specklepy.core.api.inputs import CreateModelInput
from specklepy.core.api.models import Model
from ..utils.account_manager import _client_cache
class SPECKLE_OT_create_model(bpy.types.Operator):
bl_idname = "speckle.create_model"
bl_label = "Create Model"
bl_description = "Create a new Speckle model"
model_name: bpy.props.StringProperty(name="Model Name") # type: ignore
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
if not self.model_name.strip():
self.report({"ERROR"}, "Model name cannot be empty")
return {"CANCELLED"}
try:
model = _create_model(
wm.selected_account_id, wm.selected_project_id, self.model_name
)
wm.selected_model_id = model.id
wm.selected_model_name = model.name
self.report({"INFO"}, f"Created model: {model.name} -> ID: {model.id}")
# Force redraw
context.window.screen = context.window.screen
context.area.tag_redraw()
except Exception as e:
self.report({"ERROR"}, f"Failed to create model: {str(e)}")
return {"CANCELLED"}
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.prop(self, "model_name")
def register() -> None:
bpy.utils.register_class(SPECKLE_OT_create_model)
def unregister() -> None:
bpy.utils.unregister_class(SPECKLE_OT_create_model)
def _create_model(account_id: str, project_id: str, model_name: str) -> Model:
try:
# Get cached client
client = _client_cache.get_client(account_id)
model = client.model.create(
input=CreateModelInput(
name=model_name, description="", project_id=project_id
)
)
return model
except Exception as e:
# Clear cache on error to prevent stale clients
_client_cache.clear()
raise e
@@ -1,87 +0,0 @@
from typing import Optional
import bpy
from bpy.types import Context, Event, UILayout
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.inputs import ProjectCreateInput
from specklepy.core.api.inputs.project_inputs import WorkspaceProjectCreateInput
from specklepy.core.api.models import Project
from ..utils.account_manager import _client_cache
class SPECKLE_OT_create_project(bpy.types.Operator):
"""
operator for adding a Speckle project by URL
"""
bl_idname = "speckle.create_project"
bl_label = "Create Project"
bl_description = "Create a new Speckle project"
project_name: bpy.props.StringProperty(name="Project Name") # type: ignore
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
project = _create_project(
wm.selected_account_id,
self.project_name,
None
if wm.selected_workspace.id == "personal"
else wm.selected_workspace.id,
)
wm.selected_project_id = project.id
wm.selected_project_name = project.name
self.report({"INFO"}, f"Created project: {project.name} -> ID: {project.id}")
# Force redraw
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.prop(self, "project_name")
def register() -> None:
bpy.utils.register_class(SPECKLE_OT_create_project)
def unregister() -> None:
bpy.utils.unregister_class(SPECKLE_OT_create_project)
def _create_project(
account_id: str, project_name: str, workspace_id: Optional[str]
) -> Project:
try:
# Get cached client
client = _client_cache.get_client(account_id)
if workspace_id:
project = client.project.create_in_workspace(
input=WorkspaceProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility.PUBLIC,
workspaceId=workspace_id,
)
)
else:
project = client.project.create(
input=ProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility.PUBLIC,
)
)
return project
except Exception as e:
print(f"Failed to create project: {str(e)}")
# Clear cache on error to prevent stale clients
_client_cache.clear()
raise
@@ -1,79 +0,0 @@
import bpy
from typing import Set
from bpy.types import Context, Event
from ..operations.load_operation import load_operation
from ..utils.account_manager import get_server_url_by_account_id
from ..utils.model_card_utils import (
update_model_card_objects,
delete_model_card_objects,
model_card_exists,
)
class SPECKLE_OT_load(bpy.types.Operator):
bl_idname = "speckle.load"
bl_label = "Load model"
bl_description = "Load selection from Speckle"
instance_loading_mode: bpy.props.EnumProperty( # type: ignore
name="Instance Loading",
description="Choose how to load instances",
items=[
(
"INSTANCE_PROXIES",
"Collection Instances",
"Load objects as collection instances",
),
(
"LINKED_DUPLICATES",
"Linked Duplicates",
"Get objects as linked duplicates",
),
],
default="INSTANCE_PROXIES",
)
def draw(self, context: Context) -> None:
layout = self.layout
row = layout.row()
row.label(text="Instance Loading:")
row.prop(self, "instance_loading_mode", text="")
def invoke(self, context: Context, event: Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
wm = context.window_manager
if model_card_exists(
wm.selected_project_id, wm.selected_model_id, False, context
):
model_card = context.scene.speckle_state.get_model_card_by_id(
f"{wm.ui_mode}-{wm.selected_project_id}-{wm.selected_model_id}"
)
delete_model_card_objects(model_card, context)
else:
model_card = context.scene.speckle_state.model_cards.add()
model_card.account_id = wm.selected_account_id
model_card.server_url = get_server_url_by_account_id(wm.selected_account_id)
model_card.project_id = wm.selected_project_id
model_card.project_name = wm.selected_project_name
model_card.model_id = wm.selected_model_id
model_card.model_name = wm.selected_model_name
model_card.is_publish = False
model_card.load_option = wm.selected_version_load_option
model_card.version_id = wm.selected_version_id
model_card.instance_loading_mode = self.instance_loading_mode
converted_objects = load_operation(context, self.instance_loading_mode)
update_model_card_objects(model_card, converted_objects)
# Clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_project_name = ""
wm.selected_model_id = ""
wm.selected_model_name = ""
wm.selected_version_load_option = ""
wm.selected_version_id = ""
return {"FINISHED"}
@@ -1,81 +0,0 @@
import bpy
from typing import Set
from bpy.types import Context
from ..utils.version_manager import get_latest_version
from ..operations.load_operation import load_operation
from ..utils.model_card_utils import (
delete_model_card_objects,
update_model_card_objects,
collect_objects_with_properties,
)
class SPECKLE_OT_load_model_card(bpy.types.Operator):
bl_idname = "speckle.model_card_load"
bl_label = "Load Latest from Speckle"
bl_description = "Depending on the load option, loads the latest or a specific version from Speckle"
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type: ignore
def execute(self, context: Context) -> Set[str]:
wm = context.window_manager
# Get the model card
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
old_properties = collect_objects_with_properties(model_card)
delete_model_card_objects(model_card, context)
# set wm
wm.selected_account_id = model_card.account_id
wm.selected_project_id = model_card.project_id
wm.selected_model_name = model_card.model_name
# if load option is set to "LATEST"
if model_card.load_option == "LATEST":
# get latest version from speckle
latest_version_id, message, timestamp = get_latest_version(
model_card.account_id, model_card.project_id, model_card.model_id
)
# set version id in wm
wm.selected_version_id = latest_version_id
# load latest version
converted_objects = load_operation(
context, model_card.instance_loading_mode
)
# update model card details
update_model_card_objects(model_card, converted_objects, old_properties)
model_card.version_id = latest_version_id
else:
# set version id in wm
wm.selected_version_id = model_card.version_id
# load version id
converted_objects = load_operation(
context, model_card.instance_loading_mode
)
if not converted_objects:
self.report({"ERROR"}, "Load operation failed")
return {"CANCELLED"}
# update model card details
update_model_card_objects(model_card, converted_objects, old_properties)
# Clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_version_id = ""
wm.selected_model_name = ""
self.report(
{"INFO"},
f"{len(converted_objects)} objects loaded from Speckle. Model: {model_card.model_name}, Version: {model_card.version_id}",
)
return {"FINISHED"}
@@ -1,72 +0,0 @@
import bpy
from typing import Set
from bpy.types import Context, Event
from ..operations.publish_operation import publish_operation
class SPECKLE_OT_publish_model_card(bpy.types.Operator):
bl_idname = "speckle.model_card_publish"
bl_label = "Publish model"
bl_description = "Publish tracked objects to Speckle"
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type: ignore
version_message: bpy.props.StringProperty(name="Version Message", default="") # type: ignore
def draw(self, context: Context) -> None:
layout = self.layout
layout.prop(self, "version_message")
def invoke(self, context: Context, event: Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
wm = context.window_manager
# Get the model card
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
# set wm
wm.selected_account_id = model_card.account_id
wm.selected_project_id = model_card.project_id
wm.selected_model_id = model_card.model_id
# get model card objects
objects_to_convert = []
for speckle_obj in model_card.objects:
blender_obj = bpy.data.objects.get(speckle_obj.name)
if blender_obj:
objects_to_convert.append(blender_obj)
else:
self.report(
{"WARNING"}, f"Object '{speckle_obj.name}' not found, skipping"
)
if not objects_to_convert:
self.report({"ERROR"}, "No objects to publish")
return {"CANCELLED"}
# publish to speckle
success, message, version_id = publish_operation(
context,
objects_to_convert,
self.version_message,
model_card.apply_modifiers,
)
if not success:
self.report({"ERROR"}, message)
return {"CANCELLED"}
model_card.version_id = version_id
model_card.is_publish = True
# Clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_model_id = ""
self.report({"INFO"}, message)
return {"FINISHED"}
@@ -1,127 +0,0 @@
import bpy
import webbrowser
from typing import Set
from bpy.types import Event, Context, UILayout
class SPECKLE_OT_model_card_settings(bpy.types.Operator):
"""
manages settings and actions for a Speckle model card
"""
bl_idname = "speckle.model_card_settings"
bl_label = "Model Card Settings"
bl_description = "More options for the model card"
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type:ignore
def execute(self, context: Context) -> Set[str]:
return {"FINISHED"}
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.operator(
"speckle.view_in_browser", text="View in Browser"
).model_card_id = self.model_card_id
layout.operator(
"speckle.view_model_versions", text="View Model Versions"
).model_card_id = self.model_card_id
layout.separator()
row = layout.row()
# add a button for deleting the model card
row.alert = True
delete_op = row.operator(
"speckle.delete_model_card", text="Delete Model Card", icon="TRASH"
)
delete_op.model_card_id = self.model_card_id
def invoke(self, context: Context, event: Event) -> Set[str]:
wm = context.window_manager
return wm.invoke_popup(self)
class SPECKLE_OT_view_in_browser(bpy.types.Operator):
"""
opens the current model in the Speckle web viewer
"""
bl_idname = "speckle.view_in_browser"
bl_label = "View in Browser"
bl_description = "View the model in the browser"
model_card_id: bpy.props.StringProperty() # type: ignore
def execute(self, context: Context) -> Set[str]:
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
url = f"{model_card.server_url}/projects/{model_card.project_id}/models/{model_card.model_id}"
webbrowser.open(url)
self.report({"INFO"}, f"Viewing in the browser: {url}")
return {"FINISHED"}
class SPECKLE_OT_view_model_versions(bpy.types.Operator):
"""
opens the model's version history in the Speckle web app
"""
bl_idname = "speckle.view_model_versions"
bl_label = "View Model Versions"
bl_description = "View the model versions in the browser"
model_card_id: bpy.props.StringProperty() # type: ignore
def execute(self, context: Context) -> Set[str]:
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
url = f"{model_card.server_url}/projects/{model_card.project_id}/models/{model_card.model_id}/versions"
webbrowser.open(url)
self.report({"INFO"}, "Viewing model's versions in the browser")
return {"FINISHED"}
class SPECKLE_OT_delete_model_card(bpy.types.Operator):
"""
deletes a Speckle model card from the Blender UI
"""
bl_idname = "speckle.delete_model_card"
bl_label = "Delete Model Card"
bl_description = "Delete this model card"
model_card_id: bpy.props.StringProperty() # type: ignore
def execute(self, context: Context) -> Set[str]:
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
model_name = model_card.model_name
# find the index of the model card and remove it
for i, card in enumerate(context.scene.speckle_state.model_cards):
if card.get_model_card_id() == self.model_card_id:
context.scene.speckle_state.model_cards.remove(i)
break
self.report({"INFO"}, f"Model card '{model_name}' has been deleted")
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> Set[str]:
return self.execute(context)
@@ -1,117 +0,0 @@
import bpy
from bpy.types import Context
from bpy.types import Event
from typing import Set
from ..operations.publish_operation import publish_operation
from ..utils.account_manager import get_server_url_by_account_id
from ..utils.model_card_utils import model_card_exists, update_model_card_objects
class SPECKLE_OT_publish(bpy.types.Operator):
bl_idname = "speckle.publish"
bl_label = "Publish to Speckle"
bl_description = "Publish selected objects to Speckle"
version_message: bpy.props.StringProperty(name="Version Message") # type: ignore
apply_modifiers: bpy.props.BoolProperty( # type: ignore
name="Apply Modifiers",
description="Apply all modifiers to objects before conversion",
default=True,
)
def draw(self, context: Context) -> None:
layout = self.layout
layout.prop(self, "version_message")
layout.prop(self, "apply_modifiers")
def invoke(self, context: Context, event: Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
wm = context.window_manager
# check if we have stored objects from selection dialog
if not wm.speckle_objects:
self.report(
{"ERROR"},
"No objects selected to publish. Please use 'Select Objects' first.",
)
return {"CANCELLED"}
account_id = getattr(wm, "selected_account_id", "")
project_id = getattr(wm, "selected_project_id", "")
model_id = getattr(wm, "selected_model_id", "")
if not account_id:
self.report({"ERROR"}, "No account selected")
return {"CANCELLED"}
if not project_id:
self.report({"ERROR"}, "No project selected")
return {"CANCELLED"}
if not model_id:
self.report({"ERROR"}, "No model selected")
return {"CANCELLED"}
objects_to_convert = []
for speckle_obj in wm.speckle_objects:
blender_obj = bpy.data.objects.get(speckle_obj.name)
if blender_obj:
objects_to_convert.append(blender_obj)
else:
self.report(
{"WARNING"}, f"Object '{speckle_obj.name}' not found, skipping"
)
if not objects_to_convert:
self.report({"ERROR"}, "None of the selected objects could be found")
return {"CANCELLED"}
success, message, version_id = publish_operation(
context, objects_to_convert, self.version_message, self.apply_modifiers
)
if not success:
self.report({"ERROR"}, message)
return {"CANCELLED"}
# create model card if operation was successful
if hasattr(context.scene, "speckle_state") and hasattr(
context.scene.speckle_state, "model_cards"
):
if model_card_exists(
wm.selected_project_id, wm.selected_model_id, True, context
):
model_card = context.scene.speckle_state.get_model_card_by_id(
f"{wm.ui_mode}-{wm.selected_project_id}-{wm.selected_model_id}"
)
else:
model_card = context.scene.speckle_state.model_cards.add()
model_card.account_id = account_id
model_card.server_url = get_server_url_by_account_id(account_id)
model_card.project_id = project_id
model_card.project_name = getattr(wm, "selected_project_name", "")
model_card.model_id = model_id
model_card.model_name = getattr(wm, "selected_model_name", "")
model_card.is_publish = True
model_card.load_option = "SPECIFIC" # published versions are specific
model_card.version_id = version_id
model_card.apply_modifiers = self.apply_modifiers
update_model_card_objects(model_card, objects_to_convert)
# clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_project_name = ""
wm.selected_model_id = ""
wm.selected_model_name = ""
wm.selected_version_load_option = ""
wm.selected_version_id = ""
wm.speckle_objects.clear()
self.report({"INFO"}, message)
context.area.tag_redraw()
return {"FINISHED"}
@@ -1,34 +0,0 @@
from bpy.types import Operator
from bpy.props import StringProperty
from ..utils.model_card_utils import select_model_card_objects, zoom_to_selected_objects
class SPECKLE_OT_select_objects(Operator):
"""
select all objects imported from this Speckle model
"""
bl_idname = "speckle.select_objects"
bl_label = "Select Objects"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Selects and zooms extents to objects loaded from this Speckle model"
)
model_card_id: StringProperty(
name="Model Card ID", description="ID of the model card", default=""
) # type: ignore
def execute(self, context):
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
select_model_card_objects(model_card, context)
zoom_to_selected_objects(context)
self.report({"INFO"}, f"Selected {len(context.selected_objects)} objects")
return {"FINISHED"}
@@ -1,2 +0,0 @@
from ..operations.load_operation import load_operation # noqa: F401
from ..operations.publish_operation import publish_operation # noqa: F401
@@ -1,284 +0,0 @@
from typing import Dict, Union
import bpy
from bpy.types import Context
from specklepy.core.api import host_applications, operations
from specklepy.logging import metrics
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from specklepy.objects.models.collections.collection import Collection as SCollection
from specklepy.transports.server import ServerTransport
from ... import bl_info
from ...converter.to_native import (
convert_to_native,
find_instance_definitions,
instance_definition_proxy_to_native,
render_material_proxy_to_native,
)
from ...converter.utils import find_object_by_id
from ..utils.account_manager import _client_cache
from ..utils.get_ascendants import get_ascendants
def load_operation(
context: Context, instance_loading_mode: str = "INSTANCE_PROXIES"
) -> Dict[str, Union[bpy.types.Collection, bpy.types.Object]]:
"""
load objects from Speckle and maintain hierarchy.
"""
wm = context.window_manager
# get cached client
client = _client_cache.get_client(context.window_manager.selected_account_id)
print(f"Using client for account: {context.window_manager.selected_account_id}")
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
version = client.version.get(wm.selected_version_id, wm.selected_project_id)
obj_id = version.referenced_object
version_data = operations.receive(obj_id, transport)
metrics.set_host_app("blender")
metrics.track(
metrics.RECEIVE,
client.account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"sourceHostApp": host_applications.get_host_app_from_string(
version.source_application
).slug,
"isMultiplayer": version.author_user.id != client.account.userInfo.id,
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
},
)
# Create material mapping first
material_mapping = render_material_proxy_to_native(version_data)
definition_collections, definition_objects = instance_definition_proxy_to_native(
version_data, material_mapping, instance_loading_mode=instance_loading_mode
)
definitions_root_collection = None
if definition_collections:
definitions_root_collection = bpy.data.collections.new("InstanceDefinitions")
for collection in definition_collections.values():
definitions_root_collection.children.link(collection)
definition_object_ids = set()
for definition in find_instance_definitions(version_data).values():
definition_object_ids.update(definition.objects)
for obj_id in definition.objects:
found_obj = find_object_by_id(version_data, obj_id)
if found_obj:
if hasattr(found_obj, "id"):
definition_object_ids.add(found_obj.id)
if hasattr(found_obj, "applicationId"):
definition_object_ids.add(found_obj.applicationId)
traversal_function = create_default_traversal_function()
root_collection_name = f"{wm.selected_model_name} - {wm.selected_version_id}"
root_collection = bpy.data.collections.new(root_collection_name)
context.scene.collection.children.link(root_collection)
context.window_manager.progress_begin(0, 100)
converted_objects = definition_objects.copy()
created_collections = {}
created_collections[root_collection_name] = root_collection
collection_hierarchy = {}
all_objects = {}
speckle_root_id = None
for traversal_item in traversal_function.traverse(version_data):
speckle_obj = traversal_item.current
# Skip objects that are part of instance definitions
if speckle_obj.id in definition_object_ids or (
hasattr(speckle_obj, "applicationId")
and speckle_obj.applicationId in definition_object_ids
):
continue
all_objects[speckle_obj.id] = speckle_obj
# get all ascendants in order (current to root)
ascendants = list(get_ascendants(traversal_item))
parent_ascendants = ascendants[1:] if len(ascendants) > 1 else []
if isinstance(speckle_obj, SCollection):
if not parent_ascendants and speckle_root_id is None:
speckle_root_id = speckle_obj.id
collection_name = getattr(
speckle_obj, "name", f"Collection_{speckle_obj.id}"
)
parent_id = None
for parent in parent_ascendants:
if isinstance(parent, SCollection) and hasattr(parent, "id"):
parent_id = parent.id
break
collection_hierarchy[speckle_obj.id] = {
"id": speckle_obj.id,
"name": collection_name,
"parent_id": parent_id,
"applicationId": getattr(speckle_obj, "applicationId", ""),
"blender_collection": None,
"full_path": [collection_name],
}
if parent_id in collection_hierarchy:
collection_hierarchy[speckle_obj.id]["full_path"] = (
collection_hierarchy[parent_id]["full_path"] + [collection_name]
)
else:
pass
def get_collection_depth(coll_id):
parent_id = collection_hierarchy[coll_id]["parent_id"]
if parent_id is None:
return 0
if parent_id not in collection_hierarchy:
return 0
return 1 + get_collection_depth(parent_id)
sorted_collections = sorted(
collection_hierarchy.keys(),
key=lambda coll_id: (
get_collection_depth(coll_id),
collection_hierarchy[coll_id]["name"],
),
)
if speckle_root_id and speckle_root_id in collection_hierarchy:
collection_hierarchy[speckle_root_id]["blender_collection"] = root_collection
converted_objects[speckle_root_id] = root_collection
# create collections in depth order (skip the root that's already mapped)
for coll_id in sorted_collections:
if coll_id == speckle_root_id:
continue
coll_info = collection_hierarchy[coll_id]
coll_name = coll_info["name"]
parent_id = coll_info["parent_id"]
full_path = coll_info["full_path"]
collection_key = tuple(full_path)
parent_collection = root_collection
if parent_id and parent_id in collection_hierarchy:
parent_info = collection_hierarchy[parent_id]
if parent_info["blender_collection"]:
parent_collection = parent_info["blender_collection"]
if collection_key in created_collections:
print(f"Collection already exists: {coll_name}")
blender_collection = created_collections[collection_key]
else:
blender_collection = bpy.data.collections.new(coll_name)
if coll_info.get("applicationId"):
blender_collection["applicationId"] = coll_info["applicationId"]
parent_collection.children.link(blender_collection)
created_collections[collection_key] = blender_collection
coll_info["blender_collection"] = blender_collection
converted_objects[coll_id] = blender_collection
conversion_count = 0
for traversal_item in traversal_function.traverse(version_data):
speckle_obj = traversal_item.current
if isinstance(speckle_obj, SCollection):
continue
if not hasattr(speckle_obj, "id"):
print("Skipping object without ID")
continue
# Skip objects that are part of instance definitions
if speckle_obj.id in definition_object_ids or (
hasattr(speckle_obj, "applicationId")
and speckle_obj.applicationId in definition_object_ids
):
continue
if speckle_obj.id in converted_objects:
continue
try:
target_collection = root_collection
ascendants = list(get_ascendants(traversal_item))
for parent in ascendants[1:] if len(ascendants) > 1 else []:
if isinstance(parent, SCollection) and hasattr(parent, "id"):
parent_id = parent.id
if parent_id in collection_hierarchy:
coll_info = collection_hierarchy[parent_id]
if coll_info["blender_collection"]:
target_collection = coll_info["blender_collection"]
break
blender_obj = convert_to_native(
speckle_obj,
material_mapping,
definition_collections=definition_collections,
root_collection=target_collection,
instance_loading_mode=instance_loading_mode,
)
if blender_obj is None:
continue
converted_objects[speckle_obj.id] = blender_obj
if hasattr(speckle_obj, "applicationId"):
converted_objects[speckle_obj.applicationId] = blender_obj
if not isinstance(blender_obj, bpy.types.Collection):
try:
already_linked = False
for coll in bpy.data.collections:
if blender_obj.name in coll.objects:
already_linked = True
if not already_linked:
target_collection.objects.link(blender_obj)
except RuntimeError as e:
print(f"Error linking object to collection: {e}")
except Exception as e:
print(f"Error converting {speckle_obj.speckle_type}: {str(e)}")
import traceback
traceback.print_exc()
conversion_count += 1
if conversion_count % 10 == 0:
context.window_manager.progress_update(min(conversion_count, 100))
context.window_manager.progress_end()
for area in context.screen.areas:
if area.type == "OUTLINER":
area.tag_redraw()
print(f"\nLoad process completed. Imported {len(converted_objects)} objects.")
return converted_objects
@@ -1,324 +0,0 @@
from typing import Dict, List, Optional, Tuple
import bpy
from bpy.types import Collection as BlenderCollection
from bpy.types import Context
from specklepy.core.api import operations
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.logging import metrics
from specklepy.objects import Base
from specklepy.objects.models.collections.collection import Collection
from specklepy.objects.models.units import Units
from specklepy.transports.server import ServerTransport
from ... import bl_info
from ...converter.to_speckle import convert_to_speckle
from ...converter.to_speckle.material_to_speckle import (
add_render_material_proxies_to_base,
)
from ..utils.account_manager import _client_cache
def publish_operation(
context: Context,
objects_to_convert: List,
version_message: str = "",
apply_modifiers: bool = True,
) -> Tuple[bool, str, Optional[str]]:
"""
publish objects to speckle
"""
wm = context.window_manager
try:
# get cached client
client = _client_cache.get_client(wm.selected_account_id)
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
# build collection hierarchy and convert objects
root_collection = build_collection_hierarchy(
context, objects_to_convert, apply_modifiers
)
if not root_collection:
return False, "No objects could be converted to Speckle format", None
# add material proxies
add_render_material_proxies_to_base(root_collection, objects_to_convert)
obj_id = operations.send(root_collection, [transport])
version_input = CreateVersionInput(
object_id=obj_id,
model_id=wm.selected_model_id,
project_id=wm.selected_project_id,
message=version_message,
source_application="blender",
)
version = client.version.create(version_input)
version_id = version.id
# track metrics
metrics.set_host_app("blender")
metrics.track(
metrics.SEND,
client.account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
},
)
# count total objects for success message
total_objects = count_objects_in_collection(root_collection)
return (
True,
f"Successfully published {total_objects} objects with hierarchy to Speckle",
version_id,
)
except Exception as e:
import traceback
traceback.print_exc()
# Clear cache on error to prevent stale clients
_client_cache.clear()
return False, f"Failed to publish: {str(e)}", None
def build_collection_hierarchy(
context: Context, objects_to_convert: List, apply_modifiers: bool = True
) -> Optional[Collection]:
"""
build a speckle collection hierarchy that mimicks blender's collection structure
"""
# set name for root collection
file_name = bpy.path.basename(bpy.data.filepath)
collection_name = file_name if file_name else "Untitled.blend"
collection_data = analyze_collection_structure(objects_to_convert)
if not collection_data["objects"] and not collection_data["collections"]:
return None
converted_objects = convert_selected_objects(
context, objects_to_convert, apply_modifiers
)
if not converted_objects:
return None
# create the root Speckle collection
root_collection = Collection(name=collection_name)
root_collection.units = get_scene_units(context.scene).value
root_collection["version"] = 3
# maps Blender collection to Speckle collection
collection_mapping = {} #
# create Speckle collections for each blender collection
for blender_coll in collection_data["collections"]:
speckle_coll = Collection(name=blender_coll.name)
speckle_coll.units = root_collection.units
collection_mapping[blender_coll] = speckle_coll
for blender_coll in collection_data["collections"]:
speckle_coll = collection_mapping[blender_coll]
parent_coll = find_parent_collection(
blender_coll, collection_data["collections"]
)
if parent_coll and parent_coll in collection_mapping:
parent_speckle_coll = collection_mapping[parent_coll]
parent_speckle_coll.elements.append(speckle_coll)
else:
root_collection.elements.append(speckle_coll)
# assign objects to their collections
object_mapping = {}
for i, blender_obj in enumerate(objects_to_convert):
if i < len(converted_objects) and converted_objects[i] is not None:
object_mapping[blender_obj] = converted_objects[i]
for blender_obj, speckle_obj in object_mapping.items():
placed = False
target_collection = find_target_collection_for_object(
blender_obj, collection_data["collections"]
)
if target_collection and target_collection in collection_mapping:
collection_mapping[target_collection].elements.append(speckle_obj)
placed = True
# if not placed in any subcollection, add to root
if not placed:
root_collection.elements.append(speckle_obj)
return root_collection
def analyze_collection_structure(objects: List) -> Dict:
"""
analyze the collection structure of the given objects
"""
collections_set = set()
objects_collections = {}
direct_collections = set()
for obj in objects:
obj_collections = []
for collection in bpy.data.collections:
if obj.name in collection.objects:
direct_collections.add(collection)
obj_collections.append(collection)
objects_collections[obj] = obj_collections
# find all ancestor collections
def find_all_ancestors(collection):
"""recursively find all ancestor collections"""
ancestors = set()
for potential_parent in bpy.data.collections:
if collection.name in potential_parent.children:
ancestors.add(potential_parent)
# Recursively find ancestors of the parent
ancestors.update(find_all_ancestors(potential_parent))
return ancestors
for collection in direct_collections:
collections_set.add(collection)
ancestors = find_all_ancestors(collection)
collections_set.update(ancestors)
collections_list = list(collections_set)
collections_list.sort(key=lambda c: get_collection_depth(c))
return {
"collections": collections_list,
"objects": objects,
"object_collections": objects_collections,
}
def get_collection_depth(collection: BlenderCollection) -> int:
"""
get the depth of a collection in the hierarchy
"""
depth = 0
for scene in bpy.data.scenes:
if collection.name in scene.collection.children:
return depth
for parent_coll in bpy.data.collections:
if collection.name in parent_coll.children:
return get_collection_depth(parent_coll) + 1
return depth
def find_parent_collection(
collection: BlenderCollection, all_collections: List[BlenderCollection]
) -> Optional[BlenderCollection]:
"""
find the parent collection
"""
for potential_parent in all_collections:
if collection.name in potential_parent.children:
return potential_parent
return None
def find_target_collection_for_object(
obj, collections: List[BlenderCollection]
) -> Optional[BlenderCollection]:
"""
find the deepest collection that contains this object
"""
target_collection = None
max_depth = -1
for collection in collections:
if obj.name in collection.objects:
depth = get_collection_depth(collection)
if depth > max_depth:
max_depth = depth
target_collection = collection
return target_collection
def convert_selected_objects(
context: Context, objects_to_convert: List, apply_modifiers: bool = True
) -> List[Optional[Base]]:
"""
convert selected objects to Speckle format with proper units
"""
scene = context.scene
units = get_scene_units(scene)
scale_factor = scene.unit_settings.scale_length
speckle_objects = []
for obj in objects_to_convert:
if not obj or obj.type not in ["MESH", "CURVE", "EMPTY"]:
speckle_objects.append(None)
continue
speckle_obj = convert_to_speckle(
obj, scale_factor, units.value, apply_modifiers
)
speckle_objects.append(speckle_obj)
return speckle_objects
def get_scene_units(scene) -> Units:
"""
get units from Blender's unit system
"""
unit_settings = scene.unit_settings
if unit_settings.system == "METRIC":
if unit_settings.length_unit == "METERS":
return Units.m
elif unit_settings.length_unit == "CENTIMETERS":
return Units.cm
elif unit_settings.length_unit == "MILLIMETERS":
return Units.mm
elif unit_settings.length_unit == "KILOMETERS":
return Units.km
else:
return Units.m
elif unit_settings.system == "IMPERIAL":
if unit_settings.length_unit == "FEET":
return Units.feet
elif unit_settings.length_unit == "INCHES":
return Units.inches
elif unit_settings.length_unit == "YARDS":
return Units.yards
elif unit_settings.length_unit == "MILES":
return Units.miles
else:
return Units.feet
else:
return Units.m # default to meters
def count_objects_in_collection(collection: Collection) -> int:
"""
recursively count all objects in a collection and its sub-collections
"""
count = 0
if hasattr(collection, "elements"):
for element in collection.elements:
if isinstance(element, Collection):
count += count_objects_in_collection(element)
else:
count += 1
return count
@@ -1,31 +0,0 @@
import bpy
from bpy.props import CollectionProperty
from bpy.types import PropertyGroup
from typing import Optional
from ..utils.property_groups import speckle_model_card
class SpeckleState(PropertyGroup):
"""
manages the state of the Speckle addon in Blender
"""
model_cards: CollectionProperty(type=speckle_model_card) # type: ignore
def get_model_card_by_id(self, model_card_id: str) -> Optional[speckle_model_card]:
"""Find a model card by its ID."""
for model_card in self.model_cards:
if model_card.get_model_card_id() == model_card_id:
return model_card
return None
def register() -> None:
bpy.utils.register_class(SpeckleState)
bpy.types.Scene.speckle_state = bpy.props.PointerProperty(type=SpeckleState) # type: ignore
def unregister() -> None:
del bpy.types.Scene.speckle_state
bpy.utils.unregister_class(SpeckleState)
-1
View File
@@ -1 +0,0 @@
from .main_panel import SPECKLE_PT_main_panel # noqa: F401
@@ -1,143 +0,0 @@
import bpy
from bpy.types import Context, Event
from typing import List, Tuple
from ..utils.account_manager import (
get_account_enum_items,
speckle_account,
speckle_workspace,
get_workspaces,
get_active_workspace,
get_account_from_id,
)
from ..utils.project_manager import get_projects_for_account
from ..ui.project_selection_dialog import speckle_project
class SPECKLE_UL_accounts_list(bpy.types.UIList):
"""
UIList for displaying accounts
"""
def draw_item(
self,
context: Context,
layout: bpy.types.UILayout,
data: bpy.types.PropertyGroup,
item: bpy.types.PropertyGroup,
icon: str,
active_data: bpy.types.PropertyGroup,
active_propname: str,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row()
row.label(text=item.user_name)
row.label(text=item.server_url)
row.label(text=item.user_email)
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.label(text=item.user_name)
class SPECKLE_OT_account_selection_dialog(bpy.types.Operator):
"""
operator for displaying and handling the account selection dialog
"""
bl_idname = "speckle.account_selection_dialog"
bl_label = "Select Account"
bl_description = "Select account"
account_index: bpy.props.IntProperty(default=0) # type: ignore
def invoke(self, context: Context, event: Event) -> set[str]:
wm = context.window_manager
# Clear existing accounts
wm.speckle_accounts.clear()
# Save selected account
current_account_index = 0
# Fetch accounts
for i, (id, user_name, server_url, user_email) in enumerate(
get_account_enum_items()
):
account: speckle_account = wm.speckle_accounts.add()
account.id = id
account.user_name = user_name
account.server_url = server_url
account.user_email = user_email
if id == wm.selected_account_id:
current_account_index = i
self.account_index = current_account_index
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout = self.layout
wm = context.window_manager
row = layout.row()
# add account button
if wm.selected_account_id == "NO_ACCOUNTS":
add_account_button_text = "Sign In"
add_account_button_icon = "WORLD"
else:
add_account_button_text = "Add Account"
add_account_button_icon = "ADD"
row.operator(
"speckle.add_account",
icon=add_account_button_icon,
text=add_account_button_text,
)
if wm.selected_account_id != "NO_ACCOUNTS":
layout.template_list(
"SPECKLE_UL_accounts_list",
"",
context.window_manager,
"speckle_accounts",
self,
"account_index",
)
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
# update the selected account id
account = get_account_from_id(wm.speckle_accounts[self.account_index].id)
wm.selected_account_id = account.id
self.report(
{"INFO"},
f"Selected account: {account.userInfo.name} - {account.userInfo.email} - {account.serverInfo.url}",
)
update_workspaces_list(context)
update_projects_list(context)
# redraw the area
context.area.tag_redraw()
return {"FINISHED"}
def update_workspaces_list(context: Context) -> None:
wm = context.window_manager
wm.speckle_workspaces.clear()
workspaces = get_workspaces(wm.selected_account_id)
for id, name in workspaces:
workspace: speckle_workspace = wm.speckle_workspaces.add()
workspace.id = id
workspace.name = name
wm.selected_workspace.id = get_active_workspace(wm.selected_account_id)["id"]
print("Updated Workspaces List!")
def update_projects_list(context: Context) -> None:
wm = context.window_manager
wm.speckle_projects.clear()
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
wm.selected_account_id, workspace_id=wm.selected_workspace.id
)
for name, role, updated, id, can_receive in projects:
project: speckle_project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
project.can_receive = can_receive
print("Updated Projects List!")
-34
View File
@@ -1,34 +0,0 @@
from typing import Optional, Dict
import os
import bpy.utils.previews
speckle_icons: Optional[Dict[str, bpy.types.ImagePreview]] = None
def load_icons() -> None:
global speckle_icons
speckle_icons = bpy.utils.previews.new()
icons_dir = os.path.dirname(__file__)
speckle_logo_icon_path = os.path.join(icons_dir, "speckle-logo.png")
if os.path.exists(speckle_logo_icon_path):
speckle_icons.load("speckle_logo", speckle_logo_icon_path, "IMAGE")
else:
print(f"[Speckle] WARNING icon file not found: {speckle_logo_icon_path}")
object_highlight_icon_path = os.path.join(icons_dir, "object-highlight.png")
if os.path.exists(object_highlight_icon_path):
speckle_icons.load("object_highlight", object_highlight_icon_path, "IMAGE")
else:
print(f"[Speckle] WARNING icon file not found: {object_highlight_icon_path}")
def unload_icons() -> None:
global speckle_icons
if speckle_icons is not None:
bpy.utils.previews.remove(speckle_icons)
def get_icon(icon_name: str) -> int:
global speckle_icons
if speckle_icons is None:
raise ValueError("Icons not loaded")
return speckle_icons[icon_name].icon_id
-173
View File
@@ -1,173 +0,0 @@
import bpy
from bpy.types import UILayout, Context
from .icons import get_icon
class SPECKLE_PT_main_panel(bpy.types.Panel):
"""
main panel for the Speckle addon.
"""
bl_label = "Speckle"
bl_idname = "SPECKLE_PT_main_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Speckle"
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.label(text="Speckle Connector", icon_value=get_icon("speckle_logo"))
# check to see if there are any speckle models in the file
if not context.scene.speckle_state.model_cards:
layout.label(text="Hello!")
layout.label(text="There are no Speckle models in this file yet.")
layout.separator()
wm = context.window_manager
project_selected = bool(getattr(wm, "selected_project_name", None))
model_selected = bool(getattr(wm, "selected_model_name", None))
version_selected = bool(getattr(wm, "selected_version_id", None))
selection_made = bool(getattr(wm, "speckle_objects", None))
# UI Mode Switch
row = layout.row()
row.prop(wm, "ui_mode", expand=True)
# select Project button
row = layout.row()
project_name = getattr(wm, "selected_project_name", "")
project_button_text = project_name if project_selected else "Select Project"
project_button_icon = "CHECKMARK" if project_selected else "PLUS"
row.operator(
"speckle.project_selection_dialog",
text=project_button_text,
icon=project_button_icon,
)
# select Model button
row = layout.row()
model_name = getattr(wm, "selected_model_name", "")
model_button_text = model_name if model_selected else "Select Model"
model_button_icon = "CHECKMARK" if model_selected else "PLUS"
row.enabled = project_selected
row.operator(
"speckle.model_selection_dialog",
text=model_button_text,
icon=model_button_icon,
)
if wm.ui_mode == "PUBLISH":
# TODO: implement Publish flow
# Selection filter
row = layout.row()
row.enabled = project_selected and model_selected
selection_button_text = (
f"{len(wm.speckle_objects)} Objects"
if wm.speckle_objects
else "Select Objects"
)
row.operator(
"speckle.selection_filter_dialog",
text=selection_button_text,
icon="PLUS",
).model_card_id = ""
# Publish button
row = layout.row()
row.enabled = project_selected and model_selected and selection_made
row.operator("speckle.publish", text="Publish Model", icon="EXPORT")
pass
if wm.ui_mode == "LOAD":
# select Version button
row = layout.row()
version_id = getattr(wm, "selected_version_id", "")
load_option = getattr(wm, "selected_version_load_option", "")
if load_option == "LATEST":
version_button_text = "Latest"
elif load_option == "SPECIFIC":
version_button_text = version_id
else:
version_button_text = "Select Version"
version_button_icon = "CHECKMARK" if version_selected else "PLUS"
row.enabled = project_selected and model_selected
row.operator(
"speckle.version_selection_dialog",
text=version_button_text,
icon=version_button_icon,
).model_card_id = ""
# load button
row = layout.row()
row.enabled = project_selected and model_selected and version_selected
row.operator("speckle.load", text="Load Model", icon="IMPORT")
layout.separator()
# group model cards by project name
project_groups = {}
for model_card in context.scene.speckle_state.model_cards:
project_name = (
model_card.project_name if model_card.project_name else "No Project"
)
if project_name not in project_groups:
project_groups[project_name] = []
project_groups[project_name].append(model_card)
for project_name, model_cards in project_groups.items():
project_box = layout.box()
project_row = project_box.row()
project_row.label(text=f"Project: {project_name}", icon="TRIA_RIGHT")
for model_card in model_cards:
box: UILayout = project_box.box()
row_1: UILayout = box.row()
row_2: UILayout = box.row()
if model_card.is_publish:
# Publish button in the model card
row_1.operator(
"speckle.model_card_publish", text="", icon="EXPORT"
).model_card_id = model_card.get_model_card_id()
# Selection filter button in the model card
row_2.operator(
"speckle.selection_filter_dialog",
text=f"Selection: {len(model_card.objects)} objects",
).model_card_id = model_card.get_model_card_id()
elif not model_card.is_publish:
# Load button in the model card
row_1.operator(
"speckle.model_card_load", text="", icon="IMPORT"
).model_card_id = model_card.get_model_card_id()
version_button_text = (
f"Latest: {model_card.version_id}"
if model_card.load_option == "LATEST"
else f"{model_card.version_id}"
)
row_2.operator(
"speckle.version_selection_dialog",
text=version_button_text,
).model_card_id = model_card.get_model_card_id()
# TODO: Get last updated time
else:
print({"ERROR"}, "Model card state unknown")
return
row_1.label(text=f"{model_card.model_name}")
# Select button in the model card
select_op = row_1.operator(
"speckle.select_objects",
text="",
icon_value=get_icon("object_highlight"),
)
select_op.model_card_id = model_card.get_model_card_id()
# Settings button in the model card
row_1.operator(
"speckle.model_card_settings", text="", icon="COLLAPSEMENU"
).model_card_id = model_card.get_model_card_id()
@@ -1,119 +0,0 @@
import bpy
from bpy.types import Context, Event, PropertyGroup, UILayout
from ..utils.model_manager import get_models_for_project
from ..utils.version_manager import get_latest_version
class SPECKLE_UL_models_list(bpy.types.UIList):
"""
UIList for displaying a list of Speckle models
"""
def draw_item(
self,
context: Context,
layout: UILayout,
data: PropertyGroup,
item: PropertyGroup,
icon: str,
active_data: PropertyGroup,
active_propname: str,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
split = row.split(factor=0.5)
split.label(text=item.name)
right_split = split.split(factor=0.25)
right_split.label(text=item.id)
right_split.label(text=item.updated)
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.label(text=item.name)
class SPECKLE_OT_model_selection_dialog(bpy.types.Operator):
"""
operator for displaying and handling the model selection dialog
"""
bl_idname = "speckle.model_selection_dialog"
bl_label = "Select Model"
bl_description = "Select a model to load"
def update_models_list(self, context: Context) -> None:
wm = context.window_manager
wm.speckle_models.clear()
search = self.search_query if self.search_query.strip() else None
models = get_models_for_project(
wm.selected_account_id, wm.selected_project_id, search=search
)
for name, id, updated in models:
model = wm.speckle_models.add()
model.name = name
model.updated = updated
model.id = id
return None
search_query: bpy.props.StringProperty( # type: ignore
name="Search",
description="Search a model",
default="",
update=update_models_list,
)
model_index: bpy.props.IntProperty(name="Model Index", default=0) # type: ignore
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
if 0 <= self.model_index < len(wm.speckle_models):
selected_model = wm.speckle_models[self.model_index]
wm.selected_model_id = selected_model.id
wm.selected_model_name = selected_model.name
latest_version = get_latest_version(
account_id=wm.selected_account_id,
project_id=wm.selected_project_id,
model_id=wm.selected_model_id,
)
if latest_version:
wm.selected_version_load_option = "LATEST"
wm.selected_version_id = latest_version[0]
print(f"Selected model: {selected_model.name} ({selected_model.id})")
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
self.update_models_list(context)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
wm = context.window_manager
layout.label(text=f"Project: {wm.selected_project_name}")
row = layout.row(align=True)
row.prop(self, "search_query", icon="VIEWZOOM", text="") # search bar
if wm.ui_mode != "LOAD":
row.operator("speckle.create_model", icon="ADD", text="")
layout.template_list(
"SPECKLE_UL_models_list",
"",
context.window_manager,
"speckle_models",
self,
"model_index",
)
layout.separator()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,191 +0,0 @@
import bpy
from bpy.types import UILayout, Context, PropertyGroup, Event
from typing import List, Tuple
from ..utils.account_manager import (
can_create_project_in_workspace,
get_active_workspace,
get_default_account_id,
get_account_from_id,
)
from ..utils.project_manager import get_projects_for_account
from ..utils.property_groups import speckle_project
class SPECKLE_UL_projects_list(bpy.types.UIList):
"""
UIList for displaying a list of Speckle projects
"""
def draw_item(
self,
context: Context,
layout: UILayout,
data: PropertyGroup,
item: PropertyGroup,
icon: str,
active_data: PropertyGroup,
active_propname: str,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
# enable/disable the row based on permission
row.enabled = item.can_receive
split = row.split(factor=0.5)
split.label(text=item.name)
right_split = split.split(factor=0.5)
right_split.label(text=item.role)
right_split.label(text=item.updated)
# handles when the list is in a grid layout
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.enabled = item.can_receive
layout.label(text=item.name)
class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
"""
operator for displaying and handling the project selection dialog
"""
bl_idname = "speckle.project_selection_dialog"
bl_label = "Select Project"
bl_description = "Select a project to load models from"
def update_projects_list(self, context: Context) -> None:
"""
updates the list of projects based on the selected account and search query
"""
wm = context.window_manager
wm.can_create_project_in_workspace = can_create_project_in_workspace(
wm.selected_account_id, wm.selected_workspace.id
)
wm.speckle_projects.clear()
# get projects for the selected account, using search if provided
search = self.search_query if self.search_query.strip() else None
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
wm.selected_account_id, search=search, workspace_id=wm.selected_workspace.id
)
for name, role, updated, id, can_receive in projects:
project: speckle_project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
project.can_receive = can_receive
print("Updated Projects List!")
return None
search_query: bpy.props.StringProperty( # type: ignore
name="Search or Paste a URL",
description="Search a project or paste a URL to add a project",
default="",
update=update_projects_list,
)
project_index: bpy.props.IntProperty(name="Project Index", default=0) # type: ignore
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
if 0 <= self.project_index < len(wm.speckle_projects):
selected_project = wm.speckle_projects[self.project_index]
# verify the user has permission to receive from this project
if not selected_project.can_receive:
self.report(
{"ERROR"},
"Your role on this project doesn't give you permission to load.",
)
return {"CANCELLED"}
wm.selected_project_id = selected_project.id
wm.selected_project_name = selected_project.name
print(f"Selected project: {selected_project.name} ({selected_project.id})")
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
wm = context.window_manager
# Clear existing projects
wm.speckle_projects.clear()
if wm.selected_account_id == "":
wm.selected_account_id = get_default_account_id()
wm.selected_workspace.id = get_active_workspace(wm.selected_account_id)["id"]
wm.selected_workspace.name = get_active_workspace(wm.selected_account_id)[
"name"
]
# Fetch projects from server
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
wm.selected_account_id, wm.selected_workspace.id
)
for name, role, updated, id, can_receive in projects:
project: speckle_project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
project.can_receive = can_receive
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
wm = context.window_manager
# Account selection
row = layout.row()
if wm.selected_account_id == "NO_ACCOUNTS":
row.operator("speckle.add_account", icon="WORLD", text="Sign In")
# if no accounts then don't show workspaces or projects list
if wm.selected_account_id != "NO_ACCOUNTS":
account = get_account_from_id(wm.selected_account_id)
row.operator(
"speckle.account_selection_dialog",
icon="USER",
text=f"{account.userInfo.name} - {account.userInfo.email} - {account.serverInfo.url}",
)
# Workspace selection
row = layout.row()
row.operator(
"speckle.workspace_selection_dialog",
icon="WORKSPACE",
text=wm.selected_workspace.name,
)
# Search field
row = layout.row(align=True)
row.prop(self, "search_query", icon="VIEWZOOM", text="")
# add project by url button
split = row.split()
split.operator("speckle.add_project_by_url", icon="LINKED", text="")
# create project button
# hide if in load mode
if wm.ui_mode != "LOAD":
split = row.split()
split.operator("speckle.create_project", icon="ADD", text="")
split.enabled = wm.can_create_project_in_workspace
layout.template_list(
"SPECKLE_UL_projects_list",
"",
context.window_manager,
"speckle_projects",
self,
"project_index",
)
layout.separator()
@@ -1,134 +0,0 @@
import bpy
from typing import List
from bpy.types import Operator, Context, Object
from bpy.props import EnumProperty
from ..utils.model_card_utils import update_model_card_objects
class SPECKLE_OT_selection_filter_dialog(Operator):
"""
operator for handling object selection and filtering
"""
bl_idname = "speckle.selection_filter_dialog"
bl_label = "Select Objects"
bl_description = "Select objects to publish"
selection_type: EnumProperty(
name="Selection",
items=[
("SELECTION", "Selection", "Select objects manually"),
],
default="SELECTION",
) # type: ignore
model_card_id: bpy.props.StringProperty(
name="Model Card ID",
description="This is used to indicate the function is called from a model card",
default="",
) # type: ignore
version_message: bpy.props.StringProperty(
name="Version Message",
description="Message to be used for the version",
default="",
) # type: ignore
def execute(self, context: Context) -> set:
wm = context.window_manager
wm.speckle_objects.clear()
user_selection = context.selected_objects
if self.model_card_id != "":
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
update_model_card_objects(model_card, user_selection)
self.report({"INFO"}, "Selection updated")
# Call the publish operator
bpy.ops.speckle.model_card_publish(
model_card_id=self.model_card_id, version_message=self.version_message
)
context.area.tag_redraw()
return {"FINISHED"}
for sel in user_selection:
obj = wm.speckle_objects.add()
obj.name = sel.name
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: bpy.types.Event) -> set:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context):
layout = self.layout
wm = context.window_manager
project_name = wm.selected_project_name
model_name = wm.selected_model_name
if self.model_card_id != "":
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
project_name = model_card.project_name
model_name = model_card.model_name
layout.label(text=f"Project: {project_name}")
layout.label(text=f"Model: {model_name}")
# layout.prop(self, "selection_type")
layout.separator()
selected_objects: List[Object] = context.selected_objects
total_selected: int = len(selected_objects)
box = layout.box()
row = box.row()
row.label(text="Selection Summary", icon="OUTLINER_OB_GROUP_INSTANCE")
row.label(text=f"Total: {total_selected}", icon="OBJECT_DATA")
object_types: dict[str, int] = {}
for obj in selected_objects:
if obj.type not in object_types:
object_types[obj.type] = 1
else:
object_types[obj.type] += 1
col = box.column(align=True)
for obj_type, count in object_types.items():
row = col.row()
row.label(text=f"{obj_type}:", icon=self.get_icon_for_type(obj_type))
row.label(text=str(count))
layout.separator()
if self.model_card_id != "":
layout.label(text="Version Message")
layout.prop(self, "version_message", text="")
layout.label(
text="New version will be published after updating selection",
icon="INFO_LARGE",
)
def get_icon_for_type(self, obj_type: str) -> str:
icon_map: dict[str, str] = {
"MESH": "OUTLINER_OB_MESH",
"CURVE": "OUTLINER_OB_CURVE",
"SURFACE": "OUTLINER_OB_SURFACE",
"META": "OUTLINER_OB_META",
"FONT": "OUTLINER_OB_FONT",
"ARMATURE": "OUTLINER_OB_ARMATURE",
"LATTICE": "OUTLINER_OB_LATTICE",
"EMPTY": "OUTLINER_OB_EMPTY",
"GPENCIL": "OUTLINER_OB_GREASEPENCIL",
"CAMERA": "OUTLINER_OB_CAMERA",
"LIGHT": "OUTLINER_OB_LIGHT",
"SPEAKER": "OUTLINER_OB_SPEAKER",
"LIGHT_PROBE": "OUTLINER_OB_LIGHTPROBE",
}
return icon_map.get(obj_type, "OBJECT_DATA")
def check(self, context: Context) -> bool:
return True # this forces the dialog to redraw
Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

@@ -1,185 +0,0 @@
import bpy
from bpy.types import UILayout, Context, PropertyGroup, Event
from ..utils.version_manager import get_versions_for_model, get_latest_version
class SPECKLE_UL_versions_list(bpy.types.UIList):
"""
UIList for displaying a list of Speckle versions
"""
# TODO: Adjust column widths so message has the most space.
def draw_item(
self,
context: Context,
layout: UILayout,
data: PropertyGroup,
item: PropertyGroup,
icon: str,
active_data: PropertyGroup,
active_propname: str,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
split = row.split(factor=0.166)
split.label(text=item.id)
right_split = split.split(factor=0.7)
right_split.label(text=item.message)
right_split.label(text=item.updated)
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.label(text=item.id)
class SPECKLE_OT_version_selection_dialog(bpy.types.Operator):
bl_idname = "speckle.version_selection_dialog"
bl_label = "Select Version"
bl_description = "Select a model version to load. Default is the latest version. You can also select a specific version."
version_index: bpy.props.IntProperty(name="Model Index", default=0) # type: ignore
load_option: bpy.props.EnumProperty( # type: ignore
name="Load Option",
description="Choose how to load the version",
items=[
("LATEST", "Load latest version", "Load the latest version available"),
(
"SPECIFIC",
"Load a specific version",
"Load a specific version from the list",
),
],
default="LATEST",
)
model_card_id: bpy.props.StringProperty(
name="Model Card ID",
description="This is used to indicate the function is called from a model card",
default="",
) # type: ignore
def update_versions_list(self, context: Context) -> None:
wm = context.window_manager
wm.speckle_versions.clear()
versions = get_versions_for_model(
account_id=wm.selected_account_id,
project_id=wm.selected_project_id,
model_id=wm.selected_model_id,
)
for id, message, updated in versions:
version = wm.speckle_versions.add()
version.id = id
version.message = message
version.updated = updated
return None
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
version_id_to_store = ""
if self.load_option == "LATEST":
latest_version = get_latest_version(
account_id=wm.selected_account_id,
project_id=wm.selected_project_id,
model_id=wm.selected_model_id,
)
if latest_version:
version_id_to_store = latest_version[0]
else:
print(
f"Could not fetch latest version for model {wm.selected_model_id}"
)
return {"CANCELLED"}
elif self.load_option == "SPECIFIC":
if 0 <= self.version_index < len(wm.speckle_versions):
selected_version = wm.speckle_versions[self.version_index]
version_id_to_store = selected_version.id
else:
print(f"Invalid version index {self.version_index}")
return {"CANCELLED"}
wm.selected_version_id = version_id_to_store
if self.model_card_id != "":
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, f"Model card '{self.model_card_id}' not found")
return {"CANCELLED"}
model_card.load_option = self.load_option
model_card.version_id = version_id_to_store
self.report(
{"INFO"},
f"Model card updated: Selected version: {model_card.version_id}, Option: {self.load_option}",
)
bpy.ops.speckle.model_card_load(model_card_id=self.model_card_id)
context.area.tag_redraw()
return {"FINISHED"}
wm.selected_version_load_option = self.load_option
self.report(
{"INFO"},
f"Selected version: {version_id_to_store} (Option: {self.load_option})",
)
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
if self.model_card_id != "":
wm = context.window_manager
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
self.load_option = model_card.load_option
wm.selected_account_id = model_card.account_id
wm.selected_project_id = model_card.project_id
wm.selected_model_id = model_card.model_id
wm.selected_version_id = model_card.version_id
self.update_versions_list(context)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
wm = context.window_manager
project_name = wm.selected_project_name
model_name = wm.selected_model_name
if self.model_card_id != "":
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
project_name = model_card.project_name
model_name = model_card.model_name
layout.label(text=f"Project: {project_name}")
layout.label(text=f"Model: {model_name}")
layout.prop(
self,
"load_option",
expand=True,
)
if self.load_option == "SPECIFIC":
# Versions UIList
layout.template_list(
"SPECKLE_UL_versions_list",
"",
context.window_manager,
"speckle_versions",
self,
"version_index",
)
layout.separator()
@@ -1,108 +0,0 @@
import bpy
from bpy.types import Context, UILayout, Event, PropertyGroup
from typing import List, Tuple
from ..utils.account_manager import get_workspaces, speckle_workspace
from ..utils.project_manager import get_projects_for_account
from ..utils.account_manager import can_create_project_in_workspace
class SPECKLE_UL_workspaces_list(bpy.types.UIList):
"""
UIList for workspaces
"""
def draw_item(
self,
context: Context,
layout: UILayout,
data: PropertyGroup,
item: PropertyGroup,
icon: str,
active_data: PropertyGroup,
active_propname: str,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
row.label(text=item.name)
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.label(text=item.name)
class SPECKLE_OT_workspace_selection_dialog(bpy.types.Operator):
"""
Operator for selecting a workspace
"""
bl_idname = "speckle.workspace_selection_dialog"
bl_label = "Select Workspace"
bl_description = "Select a workspace to load projects from"
workspace_index: bpy.props.IntProperty(name="Workspace Index", default=0) # type: ignore
def invoke(self, context: Context, event: Event) -> set[str]:
wm = context.window_manager
wm.speckle_workspaces.clear()
workspaces: List[Tuple[str, str]] = get_workspaces(wm.selected_account_id)
current_workspace_index = 0
for i, (id, name) in enumerate(workspaces):
workspace: speckle_workspace = wm.speckle_workspaces.add()
workspace.id = id
workspace.name = name
if id == wm.selected_workspace.id:
current_workspace_index = i
self.workspace_index = current_workspace_index
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
wm = context.window_manager
layout.label(text=f"Selected Workspace: {wm.selected_workspace.name}")
layout.template_list(
"SPECKLE_UL_workspaces_list",
"",
context.window_manager,
"speckle_workspaces",
self,
"workspace_index",
)
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
if 0 <= self.workspace_index < len(wm.speckle_workspaces):
selected_workspace = wm.speckle_workspaces[self.workspace_index]
wm.selected_workspace.id = selected_workspace.id
wm.selected_workspace.name = selected_workspace.name
update_projects_list(context)
context.area.tag_redraw()
return {"FINISHED"}
def update_projects_list(context):
"""Update projects list when workspace changes"""
wm = context.window_manager
wm.speckle_projects.clear()
# get projects for the selected account and workspace
projects = get_projects_for_account(
wm.selected_account_id, wm.selected_workspace.id
)
for name, role, updated, id, can_receive in projects:
project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
project.can_receive = can_receive
# Update can_create_project_in_workspace flag
wm.can_create_project_in_workspace = can_create_project_in_workspace(
wm.selected_account_id, wm.selected_workspace.id
)
print(f"Workspace changed to: {wm.selected_workspace.id}")
print("Projects list updated")
context.area.tag_redraw()
@@ -1,307 +0,0 @@
from typing import Dict, List, Optional, Tuple
import bpy
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_local_accounts
from specklepy.core.api.wrapper import StreamWrapper
from .misc import strip_non_ascii
class SpeckleClientCache:
def __init__(self):
self._clients: Dict[str, SpeckleClient] = {}
def get_client(self, account_id: str) -> SpeckleClient:
# Check cache first
if account_id in self._clients:
print(f"[Cache HIT] Using cached client for account {account_id}")
return self._clients[account_id]
# Create new client if needed
print(f"[Cache MISS] Creating new client for account {account_id}")
account = get_account_from_id(account_id)
if not account:
raise ValueError(f"No account found for ID: {account_id}")
assert account.serverInfo.url
client = SpeckleClient(
host=account.serverInfo.url,
use_ssl=account.serverInfo.url.startswith("https"),
)
client.authenticate_with_account(account)
self._clients[account_id] = client
return client
def clear(self) -> None:
"""Clear all cached clients."""
print("[Cache] Clearing all cached clients")
self._clients.clear()
# Global cache instance
_client_cache = SpeckleClientCache()
class speckle_account(bpy.types.PropertyGroup):
id: bpy.props.StringProperty() # type: ignore
user_name: bpy.props.StringProperty() # type: ignore
server_url: bpy.props.StringProperty() # type: ignore
user_email: bpy.props.StringProperty() # type: ignore
class speckle_workspace(bpy.types.PropertyGroup):
"""
PropertyGroup for storing workspace information
"""
id: bpy.props.StringProperty(name="ID") # type: ignore
name: bpy.props.StringProperty() # type: ignore
def get_account_enum_items() -> List[Tuple[str, str, str, str]]:
accounts: List[Account] = get_local_accounts()
if not accounts:
print("No accounts found!")
return [("NO_ACCOUNTS", "No accounts found!", "", "")]
print("Accounts added")
speckle_accounts = []
for acc in accounts:
speckle_accounts.append(
(
acc.id,
strip_non_ascii(acc.userInfo.name),
acc.serverInfo.url,
acc.userInfo.email,
)
)
return speckle_accounts
def get_workspaces(account_id: str) -> List[Tuple[str, str]]:
"""
retrieves the workspaces for a given account ID
"""
try:
# Get client from cache
client = _client_cache.get_client(account_id)
workspaces_enabled = client.server.get().workspaces.workspaces_enabled
if workspaces_enabled:
workspaces = client.active_user.get_workspaces().items
workspace_list = [
(ws.id, strip_non_ascii(ws.name))
for ws in workspaces
if ws.creation_state is None or ws.creation_state.completed
]
personal_projects_text = "Personal Projects (Legacy)"
else:
workspace_list = []
personal_projects_text = "Personal Projects"
workspace_list.append(("personal", personal_projects_text))
if workspaces_enabled:
active_workspace = client.active_user.get_active_workspace()
default_workspace_id = (
active_workspace.id if active_workspace else "personal"
)
result = reorder_tuple(workspace_list, default_workspace_id)
else:
result = workspace_list
return result
except Exception as e:
print(f"Error in get_workspaces: {str(e)}")
_client_cache.clear() # Clear cache on error
return [("", "")]
def get_default_account_id() -> Optional[str]:
"""
retrieves the ID of the default Speckle account
"""
return next(
(acc.id for acc in get_local_accounts() if acc.isDefault), "NO_ACCOUNTS"
)
def get_server_url_by_account_id(account_id: str) -> Optional[str]:
"""
retrieves the server URL for a given account ID
"""
accounts: List[Account] = get_local_accounts()
for acc in accounts:
if acc.id == account_id:
return acc.serverInfo.url
return None
def get_active_workspace(account_id: str) -> Optional[Dict[str, str]]:
"""
retrieves the ID of the default workspace for a given account ID
"""
try:
client = _client_cache.get_client(account_id)
active_workspace = client.active_user.get_active_workspace()
if active_workspace:
return {"id": active_workspace.id, "name": active_workspace.name}
return {"id": "personal", "name": "Personal Projects"}
except Exception as e:
print(f"Error in get_active_workspace: {str(e)}")
_client_cache.clear()
return None
def get_account_from_id(account_id: str) -> Optional[Account]:
return next((acc for acc in get_local_accounts() if acc.id == account_id), None)
def reorder_tuple(tuple_list, target_id):
for i, (id, value) in enumerate(tuple_list):
if id == target_id:
# Remove the tuple from its current position
target_tuple = tuple_list.pop(i)
# Insert it at the beginning of the list
tuple_list.insert(0, target_tuple)
return tuple_list
# If the target_id wasn't found
print(f"Tuple with ID {target_id} not found in the list")
return tuple_list
def get_project_from_url(
url: str,
) -> Tuple[Optional[StreamWrapper], Optional[object], Optional[object], str]:
"""
get a project from a URL, handling all the client setup.
"""
try:
wrapper = StreamWrapper(url)
account = wrapper.get_account()
assert account.id
client = _client_cache.get_client(account.id)
# get the stream_id (project_id) from the wrapper
if not wrapper.stream_id:
return wrapper, client, None, "Could not extract project ID from URL"
project = client.project.get(wrapper.stream_id)
if not project:
return wrapper, client, None, "Could not access project"
return wrapper, client, project, ""
except Exception as e:
return None, None, None, f"Failed to process URL: {str(e)}"
def get_model_details_by_wrapper(
wrapper: StreamWrapper,
) -> Tuple[str, str, str, str, str, str, str]:
"""
extract model details from a StreamWrapper object.
"""
client = wrapper.get_client()
client.authenticate_with_account(wrapper.get_account())
(
account_id,
project_id,
project_name,
model_id,
model_name,
version_id,
load_option,
) = "", "", "", "", "", "", ""
account_id = wrapper.get_account().id
if wrapper.stream_id:
project_id = wrapper.stream_id
project_name = client.project.get(project_id).name
if wrapper.model_id:
model_id = wrapper.model_id
model = client.model.get(model_id, project_id)
model_name = model.name
load_option = "LATEST" if not wrapper.commit_id else "SPECIFIC"
if wrapper.commit_id:
version_id = wrapper.commit_id
else:
versions = client.version.get_versions(
wrapper.model_id, wrapper.stream_id, limit=1
)
if versions.items and len(versions.items) > 0:
version_id = versions.items[0].id
else:
version_id = ""
return (
account_id,
project_id,
project_name,
model_id,
model_name,
version_id,
load_option,
)
def can_load(client, project) -> Tuple[bool, str]:
try:
permissions = client.project.get_permissions(project.id)
if permissions.can_load.authorized:
return True, ""
else:
return (
False,
"Your role on this project doesn't give you permission to load.",
)
except Exception as e:
error_msg = f"Failed to check permissions: {str(e)}"
print(error_msg)
return False, error_msg
def can_publish(client, project) -> Tuple[bool, str]:
try:
permissions = client.project.get_permissions(project.id)
if permissions.can_publish.authorized:
return True, ""
else:
return (
False,
"Your role on this project doesn't give you permission to publish.",
)
except Exception as e:
error_msg = f"Failed to check permissions: {str(e)}"
print(error_msg)
return False, error_msg
def can_create_project_in_workspace(account_id: str, workspace_id: str) -> bool:
"""
Check if the user can create a project in the specified workspace.
"""
try:
client = _client_cache.get_client(account_id)
if workspace_id == "personal":
return client.active_user.can_create_personal_projects().authorized
else:
try:
workspace = client.workspace.get(workspace_id)
return workspace.permissions.can_create_project.authorized
except Exception as e:
print(f"Failed to get workspace: {str(e)}")
return False
except Exception as e:
print(f"Error in can_create_project_in_workspace: {str(e)}")
_client_cache.clear() # Clear cache on error
return False
@@ -1,26 +0,0 @@
from typing import Iterator, TypeVar, Type
from specklepy.objects.base import Base
from specklepy.objects.graph_traversal.traversal import TraversalContext
def get_ascendants(context: TraversalContext) -> Iterator[Base]:
"""
walks up the tree, returning all ascendants, including context
"""
head = context
while head is not None:
yield head.current
head = head.parent
T = TypeVar("T", bound=Base)
def get_ascendant_of_type(context: TraversalContext, type_cls: Type[T]) -> Iterator[T]:
"""
walks up the tree, returning all ascendants of the given type,
starting with the context, walking up parent nodes
"""
for ascendant in get_ascendants(context):
if isinstance(ascendant, type_cls):
yield ascendant
-53
View File
@@ -1,53 +0,0 @@
from datetime import datetime, timezone
import re
def format_relative_time(timestamp) -> str:
"""
convert UTC timestamp to local timezone and return relative time string
"""
if not timestamp:
return "Unknown"
# convert to local timezone
try:
try:
dt = datetime.fromisoformat(str(timestamp).replace("Z", "+00:00"))
except ValueError:
try:
ts = float(timestamp)
dt = datetime.fromtimestamp(ts / 1000, tz=timezone.utc)
except (ValueError, TypeError):
return "Invalid timestamp"
local_dt = dt.astimezone()
# calculate relative time
now = datetime.now(timezone.utc).astimezone()
delta = now - local_dt
if delta.days == 0:
if delta.seconds < 3600:
minutes = delta.seconds // 60
return f"{minutes} minutes ago"
else:
hours = delta.seconds // 3600
return f"{hours} hours ago"
else:
return f"{delta.days} days ago"
except ValueError:
return "Invalid timestamp"
def format_role(role: str) -> str:
"""
This function takes a Speckle role string in the format "prefix:role" and
returns just the role part
"""
split_role = role.split(":")
return f"{split_role[1]}"
def strip_non_ascii(text):
# Keep English letters, digits, spaces and basic punctuation
return re.sub(r"[^a-zA-Z0-9\s.,!?]", "", text)
@@ -1,418 +0,0 @@
import bpy
from bpy.types import Context
from typing import Dict, Any, Optional
from ..utils.property_groups import speckle_model_card
def find_layer_collection(layer_collection, collection_name):
"""
Recursively find a layer collection by collection name
"""
if layer_collection.collection.name == collection_name:
return layer_collection
for child in layer_collection.children:
result = find_layer_collection(child, collection_name)
if result:
return result
return None
def get_object_by_application_id(app_id: str):
"""
Find a Blender object by its applicationId stored in custom property
"""
if not app_id:
return None
for obj in bpy.data.objects:
if "applicationId" in obj and obj["applicationId"] == app_id:
return obj
return None
def get_objects_by_application_ids(app_ids: list):
"""
Find multiple Blender objects by their applicationIds
"""
if not app_ids:
return {}
result = {}
for obj in bpy.data.objects:
if "applicationId" in obj and obj["applicationId"] in app_ids:
result[obj["applicationId"]] = obj
return result
def get_collection_by_application_id(app_id: str):
"""
Find a Blender collection by its applicationId stored in custom property
"""
if not app_id:
return None
for collection in bpy.data.collections:
if "applicationId" in collection and collection["applicationId"] == app_id:
return collection
return None
def get_collection_identifier(blender_col: bpy.types.Collection) -> str:
"""
Get collection identifier: applicationId if exists, fallback to name
"""
if "applicationId" in blender_col and blender_col["applicationId"]:
return blender_col["applicationId"]
return blender_col.name
def find_collection_by_identifier(identifier: str):
"""
Find collection by identifier: try applicationId first, then name
"""
# first try to find by applicationId
collection = get_collection_by_application_id(identifier)
if collection:
return collection
# fallback to name-based lookup
return bpy.data.collections.get(identifier)
def capture_modifier_data(blender_obj: bpy.types.Object) -> list:
"""
Capture modifier data from a Blender object as dictionaries
"""
modifiers_data = []
for modifier in blender_obj.modifiers:
modifier_data = {
"name": modifier.name,
"type": modifier.type,
"show_viewport": modifier.show_viewport,
"show_render": modifier.show_render,
"show_in_editmode": modifier.show_in_editmode,
"show_on_cage": modifier.show_on_cage,
"properties": {},
}
# Capture modifier-specific properties
for prop_name in modifier.bl_rna.properties.keys():
if prop_name in [
"rna_type",
"name",
"type",
"show_viewport",
"show_render",
"show_in_editmode",
"show_on_cage",
]:
continue
try:
if hasattr(modifier, prop_name):
prop_value = getattr(modifier, prop_name)
# Handle different property types
if isinstance(prop_value, (int, float, bool, str)):
modifier_data["properties"][prop_name] = prop_value
elif hasattr(prop_value, "name"): # Object references
modifier_data["properties"][prop_name] = prop_value.name
elif (
hasattr(prop_value, "__len__") and len(prop_value) <= 4
): # Vectors/colors
modifier_data["properties"][prop_name] = list(prop_value)
except (AttributeError, TypeError):
continue
modifiers_data.append(modifier_data)
return modifiers_data
def has_visibility_modifications(obj: bpy.types.Object) -> bool:
"""Check if object has non-default visibility settings"""
return obj.hide_viewport or obj.hide_select or obj.hide_render or obj.hide_get()
def has_modifier_modifications(obj: bpy.types.Object) -> bool:
"""Check if object has any modifiers applied"""
return hasattr(obj, "modifiers") and len(obj.modifiers) > 0
def has_collection_visibility_modifications(layer_col, collection) -> bool:
"""Check if collection has non-default visibility settings"""
return (
layer_col.hide_viewport
or collection.hide_select
or collection.hide_render
or layer_col.exclude
)
def collect_objects_with_properties(
model_card: speckle_model_card,
) -> Dict[str, Dict[str, Any]]:
"""
Collect objects and collections with their current properties before deletion
Only stores data for objects that have been modified from defaults
"""
collected_data = {"objects": {}, "collections": {}}
# Collect object properties (only for modified objects)
for s_obj in model_card.objects:
blender_obj = get_object_by_application_id(s_obj.applicationId)
if blender_obj:
obj_data = {}
# Only collect visibility if modified from defaults
if has_visibility_modifications(blender_obj):
obj_data["visibility"] = {
"hide_get": blender_obj.hide_get(),
"hide_viewport": blender_obj.hide_viewport,
"hide_select": blender_obj.hide_select,
"hide_render": blender_obj.hide_render,
}
# Only collect modifiers if object has any
if has_modifier_modifications(blender_obj):
obj_data["modifiers"] = capture_modifier_data(blender_obj)
# Only store object data if it has modifications
if obj_data:
collected_data["objects"][s_obj.applicationId] = obj_data
# Collect collection properties (only for modified collections)
for s_col in model_card.collections:
# try to find collection by applicationId first, then fallback to name
blender_col = None
if s_col.applicationId:
blender_col = get_collection_by_application_id(s_col.applicationId)
if not blender_col:
blender_col = bpy.data.collections.get(s_col.name)
if blender_col:
view_layer = bpy.context.view_layer
if view_layer:
layer_col = find_layer_collection(
view_layer.layer_collection, blender_col.name
)
if layer_col and has_collection_visibility_modifications(
layer_col, blender_col
):
# use collection identifier as key
collection_id = get_collection_identifier(blender_col)
collected_data["collections"][collection_id] = {
"hide_viewport": layer_col.hide_viewport,
"hide_select": layer_col.collection.hide_select,
"hide_render": layer_col.collection.hide_render,
"exclude_from_view_layer": layer_col.exclude,
}
return collected_data
def transfer_object_properties(
new_obj: bpy.types.Object, old_obj_data: Dict[str, Any]
) -> None:
"""
Transfer visibility and modifiers from old object data to new object
Handles sparse data gracefully - applies defaults when data is missing
"""
# Transfer visibility settings (if any were modified)
visibility = old_obj_data.get("visibility")
if visibility:
new_obj.hide_set(visibility.get("hide_get", False))
new_obj.hide_viewport = visibility.get("hide_viewport", False)
new_obj.hide_select = visibility.get("hide_select", False)
new_obj.hide_render = visibility.get("hide_render", False)
# If no visibility data, object keeps defaults (all False)
# Transfer modifiers (if any were present)
old_modifiers = old_obj_data.get("modifiers")
if old_modifiers and hasattr(new_obj, "modifiers"):
# Clear existing modifiers
new_obj.modifiers.clear()
# Transfer each modifier
for modifier_data in old_modifiers:
recreate_modifier_from_data(new_obj, modifier_data)
# If no modifier data, object keeps default (no modifiers)
def transfer_collection_properties(
new_col: bpy.types.Collection, old_col_data: Dict[str, Any]
) -> None:
"""
Transfer visibility properties from old collection data to new collection
Handles sparse data gracefully - applies defaults when data is missing
"""
view_layer = bpy.context.view_layer
if view_layer:
layer_col = find_layer_collection(view_layer.layer_collection, new_col.name)
if layer_col:
# Only apply properties if collection had modifications
# (otherwise it keeps defaults: all False)
layer_col.hide_viewport = old_col_data.get("hide_viewport", False)
layer_col.collection.hide_select = old_col_data.get("hide_select", False)
layer_col.collection.hide_render = old_col_data.get("hide_render", False)
layer_col.exclude = old_col_data.get("exclude_from_view_layer", False)
def recreate_modifier_from_data(
new_obj: bpy.types.Object, modifier_data: Dict[str, Any]
) -> Optional[bpy.types.Modifier]:
"""
Recreate a modifier from captured data
"""
try:
# Validate modifier data
if not modifier_data.get("type") or not modifier_data.get("name"):
print(f"Invalid modifier data: {modifier_data}")
return None
# Create new modifier
new_modifier = new_obj.modifiers.new(
modifier_data["name"], modifier_data["type"]
)
# Set visibility properties
new_modifier.show_viewport = modifier_data.get("show_viewport", True)
new_modifier.show_render = modifier_data.get("show_render", True)
new_modifier.show_in_editmode = modifier_data.get("show_in_editmode", True)
new_modifier.show_on_cage = modifier_data.get("show_on_cage", False)
# Set modifier-specific properties
for prop_name, prop_value in modifier_data.get("properties", {}).items():
try:
if hasattr(new_modifier, prop_name):
current_value = getattr(new_modifier, prop_name)
# Handle object references
if hasattr(current_value, "name") and isinstance(prop_value, str):
referenced_obj = bpy.data.objects.get(prop_value)
if referenced_obj:
setattr(new_modifier, prop_name, referenced_obj)
else:
setattr(new_modifier, prop_name, prop_value)
except (AttributeError, TypeError):
continue
return new_modifier
except Exception as e:
print(f"Error recreating modifier {modifier_data.get('name', 'unknown')}: {e}")
return None
def update_model_card_objects(
model_card: speckle_model_card,
converted_objects: Dict[str, bpy.types.Object | bpy.types.Collection],
old_properties: Optional[Dict[str, Dict[str, Any]]] = None,
):
"""
Update model card with new objects and apply properties from old objects if provided
"""
# Clear model card objects
model_card.objects.clear()
model_card.collections.clear()
# Convert list to dictionary if needed
if isinstance(converted_objects, list):
converted_objects = {obj.name: obj for obj in converted_objects}
for obj in converted_objects.values():
# Handle collections
if isinstance(obj, bpy.types.Collection):
if obj.name in (o.name for o in model_card.collections):
continue
s_col = model_card.collections.add()
s_col.name = obj.name
s_col.applicationId = obj.get("applicationId", "")
# apply old collection properties if available (use identifier-based lookup)
if old_properties:
collection_id = get_collection_identifier(obj)
if collection_id in old_properties.get("collections", {}):
old_col_data = old_properties["collections"][collection_id]
transfer_collection_properties(obj, old_col_data)
# Handle objects
elif isinstance(obj, bpy.types.Object):
if obj.name in (o.name for o in model_card.objects):
continue
s_obj = model_card.objects.add()
s_obj.name = obj.name
s_obj.applicationId = obj.get("applicationId", "")
# Apply old object properties if available
if (
old_properties
and s_obj.applicationId
and s_obj.applicationId in old_properties.get("objects", {})
):
old_obj_data = old_properties["objects"][s_obj.applicationId]
transfer_object_properties(obj, old_obj_data)
def delete_model_card_objects(model_card: speckle_model_card, context: Context) -> None:
"""
deletes the model card objects
"""
# Delete objects directly without requiring selection
for obj in model_card.objects:
blender_obj = get_object_by_application_id(obj.applicationId)
if not blender_obj:
continue
# Remove object from all collections first
for collection in blender_obj.users_collection:
collection.objects.unlink(blender_obj)
# Delete the object directly
bpy.data.objects.remove(blender_obj)
# delete model card/currently loaded collections
for col in model_card.collections:
coll = bpy.data.collections.get(col.name)
if not coll:
continue
# unlink from scenes
for scene in bpy.data.scenes:
if scene.collection.children.get(coll.name):
scene.collection.children.unlink(coll)
bpy.data.collections.remove(coll)
def select_model_card_objects(model_card, context: Context):
# deselect all objects first
bpy.ops.object.select_all(action="DESELECT")
# select objects in model card
for obj in model_card.objects:
blender_obj = get_object_by_application_id(obj.applicationId)
if not blender_obj:
continue
if blender_obj.name in context.view_layer.objects:
blender_obj.select_set(True)
selected = context.selected_objects
if selected:
context.view_layer.objects.active = selected[0]
def zoom_to_selected_objects(context: Context):
"""
zooms to the selected objects
"""
bpy.ops.view3d.view_selected()
def model_card_exists(
project_id: str, model_id: str, is_publish: bool, context: Context
) -> bool:
"""
checks if a model card exists
"""
for model_card in context.scene.speckle_state.model_cards:
if (
model_card.project_id == project_id
and model_card.model_id == model_id
and model_card.is_publish == is_publish
):
return True
return False
@@ -1,46 +0,0 @@
from typing import List, Optional, Tuple
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models.current import Model
from .account_manager import _client_cache
from .misc import format_relative_time, strip_non_ascii
def get_models_for_project(
account_id: str, project_id: str, search: Optional[str] = None
) -> List[Tuple[str, str, str]]:
"""
fetches models for a given project from the Speckle server
"""
try:
if not account_id or not project_id:
print(
f"Error: Invalid inputs - account_id: {account_id}, project_id: {project_id}"
)
return []
client = _client_cache.get_client(account_id)
client.project.get(project_id)
filter = ProjectModelsFilter(search=search) if search else None
models: List[Model] = client.model.get_models(
project_id=project_id, models_limit=10, models_filter=filter
).items
return [
(
strip_non_ascii(model.name),
model.id,
format_relative_time(model.updated_at),
)
for model in models
]
except Exception as e:
print(f"Error fetching models: {str(e)}")
# Clear cache on error to prevent stale clients
_client_cache.clear()
return []
@@ -1,175 +0,0 @@
from typing import List, Optional, Tuple
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
from specklepy.core.api.resources.current.workspace_resource import WorkspaceResource
from .account_manager import _client_cache
from .misc import format_relative_time, format_role, strip_non_ascii
def get_projects_for_account(
account_id: str, workspace_id: str, search: Optional[str] = None
) -> List[Tuple[str, str, str, str, bool]]:
"""
fetches projects for a given account from the Speckle server
"""
try:
# Get cached client
client = _client_cache.get_client(account_id)
if not client:
print(f"Error: Could not get client for account: {account_id}")
return []
# Get account for workspace operations that still need it
from specklepy.core.api.credentials import get_local_accounts
account: Optional[Account] = next(
(acc for acc in get_local_accounts() if acc.id == account_id), None
)
if not account:
print(f"Error: Could not find account with ID: {account_id}")
return []
if workspace_id == "personal":
return _get_personal_projects_with_permissions(client, search)
try:
workspace_resource = WorkspaceResource(
account, client.url, client.httpclient, client.server.version()
)
# create filter with search parameter
filter = (
WorksaceProjectsFilter(search=search, with_project_role_only=False)
if search
else None
)
projects_with_permissions = (
workspace_resource.get_projects_with_permissions(
workspace_id=workspace_id, limit=10, filter=filter
)
)
result = []
for project in projects_with_permissions.items:
can_load_permission = False
if hasattr(project, "permissions") and project.permissions:
can_load_permission = (
hasattr(project.permissions, "can_load")
and project.permissions.can_load
and project.permissions.can_load.authorized
)
result.append(
(
strip_non_ascii(project.name),
format_role(getattr(project, "role", ""))
if hasattr(project, "role") and project.role
else "",
format_relative_time(project.updated_at),
project.id,
can_load_permission,
)
)
return result
except Exception as workspace_error:
print(
f"WorkspaceResource failed, falling back to old method: {workspace_error}"
)
return _get_projects_with_individual_permissions(
client, workspace_id, search
)
except Exception as e:
import traceback
error_msg = f"Error: {str(e)}\n"
error_msg += f"Traceback:\n{''.join(traceback.format_tb(e.__traceback__))}"
print(error_msg)
# Clear cache on error to prevent stale clients
_client_cache.clear()
return []
def _get_personal_projects_with_permissions(
client: SpeckleClient, search: Optional[str] = None
) -> List[Tuple[str, str, str, str, bool]]:
"""
helper function to get personal projects with permissions using the old method
"""
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from .account_manager import can_load
filter = UserProjectsFilter(
search=search,
workspace_id=None,
personal_only=True,
include_implicit_access=True,
)
projects = client.active_user.get_projects(limit=10, filter=filter).items
result = []
for project in projects:
can_load_permission, _ = can_load(client, project)
result.append(
(
strip_non_ascii(project.name),
format_role(getattr(project, "role", ""))
if hasattr(project, "role") and project.role
else "",
format_relative_time(project.updated_at),
project.id,
can_load_permission,
)
)
return result
def _get_projects_with_individual_permissions(
client: SpeckleClient,
workspace_id: str,
search: Optional[str] = None,
) -> List[Tuple[str, str, str, str, bool]]:
"""
Fallback helper function to get projects with permissions using individual API calls
"""
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from .account_manager import can_load
filter = UserProjectsFilter(
search=search,
workspace_id=workspace_id,
personal_only=False,
include_implicit_access=True,
)
projects = client.active_user.get_projects(limit=10, filter=filter).items
result = []
for project in projects:
can_load_permission, _ = can_load(client, project)
result.append(
(
strip_non_ascii(project.name),
format_role(getattr(project, "role", ""))
if hasattr(project, "role") and project.role
else "",
format_relative_time(project.updated_at),
project.id,
can_load_permission,
)
)
return result
@@ -1,115 +0,0 @@
import bpy
class speckle_project(bpy.types.PropertyGroup):
"""
PropertyGroup for storing project information
"""
name: bpy.props.StringProperty() # type: ignore
role: bpy.props.StringProperty(name="Role") # type: ignore
updated: bpy.props.StringProperty(name="Updated") # type: ignore
id: bpy.props.StringProperty(name="ID") # type: ignore
can_receive: bpy.props.BoolProperty(name="Can Receive", default=False) # type: ignore
class speckle_model(bpy.types.PropertyGroup):
"""
PropertyGroup for storing model information
"""
name: bpy.props.StringProperty() # type: ignore
id: bpy.props.StringProperty(name="ID") # type: ignore
updated: bpy.props.StringProperty(name="Updated") # type: ignore
class speckle_version(bpy.types.PropertyGroup):
"""
PropertyGroup for storing version information
"""
id: bpy.props.StringProperty(name="ID") # type: ignore
message: bpy.props.StringProperty(name="Message") # type: ignore
updated: bpy.props.StringProperty(name="Updated") # type: ignore
source_app: bpy.props.StringProperty(name="Source") # type: ignore
class speckle_object(bpy.types.PropertyGroup):
"""
PropertyGroup for storing object names and applicationIds
"""
name: bpy.props.StringProperty() # type: ignore
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
class speckle_collection(bpy.types.PropertyGroup):
"""
PropertyGroup for storing collections
"""
name: bpy.props.StringProperty() # type: ignore
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
class speckle_model_card(bpy.types.PropertyGroup):
"""
represents a Speckle model card in the Blender UI
"""
account_id: bpy.props.StringProperty(
name="Account ID", description="ID of the account", default=""
) # type: ignore
server_url: bpy.props.StringProperty(
name="Server URL",
description="URL of the Server",
default="app.speckle.systems",
) # type: ignore
project_name: bpy.props.StringProperty(
name="Project Name", description="Name of the project", default=""
) # type: ignore
project_id: bpy.props.StringProperty(
name="Project ID", description="ID of the selected project", default=""
) # type: ignore
model_id: bpy.props.StringProperty(
name="Model ID", description="ID of the model", default=""
) # type: ignore
model_name: bpy.props.StringProperty(
name="Model Name", description="Name of the model", default=""
) # type: ignore
is_publish: bpy.props.BoolProperty(
name="Publish/Load",
description="If the model is published or loaded",
default=False,
) # type: ignore
selection_summary: bpy.props.StringProperty(
name="Selection Summary", description="Summary of the selection", default=""
) # type: ignore
version_id: bpy.props.StringProperty(
name="Version ID", description="ID of the selected version", default=""
) # type: ignore
load_option: bpy.props.StringProperty(
name="Load Option", description="Option of loading the model", default=""
) # type: ignore
objects: bpy.props.CollectionProperty(type=speckle_object) # type: ignore
collections: bpy.props.CollectionProperty(type=speckle_collection) # type: ignore
instance_loading_mode: bpy.props.StringProperty(
name="Instance Loading Mode",
description="Mode of loading instances",
default="INSTANCE_PROXIES",
) # type: ignore
apply_modifiers: bpy.props.BoolProperty(
name="Apply Modifiers",
description="Apply modifiers to the objects",
default=True,
) # type: ignore
def get_model_card_id(self) -> str:
if not self.project_id or not self.model_id:
raise ValueError(
"Project ID and Model ID are required to generate a model card ID."
)
if self.is_publish:
return f"PUBLISH-{self.project_id}-{self.model_id}"
else:
return f"LOAD-{self.project_id}-{self.model_id}"
@@ -1,85 +0,0 @@
from typing import List, Tuple
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models.current import Version
from .account_manager import _client_cache
from .misc import format_relative_time
def get_versions_for_model(
account_id: str, project_id: str, model_id: str
) -> List[Tuple[str, str, str]]:
"""
fetches versions for a given model from the Speckle server
"""
try:
# Validate inputs
if not account_id or not project_id or not model_id:
print(
f"Error: Invalid inputs - account_id: {account_id}, project_id: {project_id}, model_id: {model_id}"
)
return []
client: SpeckleClient = _client_cache.get_client(account_id)
# Get versions
versions = client.version.get_versions(
project_id=project_id, model_id=model_id, limit=10
)
versions_list: List[Tuple[str, str, str]] = []
for version in versions.items:
if version.referenced_object != "":
versions_list.append(
(
version.id,
version.message
if version.message is not None
else "No message",
format_relative_time(version.created_at),
)
)
return versions_list
except Exception as e:
print(f"Error fetching versions: {str(e)}")
# Clear cache on error to prevent stale clients
_client_cache.clear()
return []
def get_latest_version(
account_id: str, project_id: str, model_id: str
) -> Tuple[str, str, str]:
try:
# Validate inputs
if not account_id or not project_id or not model_id:
print(
f"Error: Invalid inputs - account_id: {account_id}, project_id: {project_id}, model_id: {model_id}"
)
return ("", "", "")
# Get cached client
client: SpeckleClient = _client_cache.get_client(account_id)
# Get versions (limit to 1 since we only need the latest)
versions: List[Version] = client.version.get_versions(
project_id=project_id, model_id=model_id, limit=1
).items
if not versions:
print(f"Error: No versions found for model_id: {model_id}")
return ("", "", "")
latest = versions[0]
return (
latest.id,
latest.message if latest.message is not None else "No message",
format_relative_time(latest.created_at),
)
except Exception as e:
print(f"Error fetching latest version: {str(e)}")
# Clear cache on error to prevent stale clients
_client_cache.clear()
return ("", "", "")
View File
+22
View File
@@ -0,0 +1,22 @@
IGNORED_PROPERTY_KEYS = {
"id",
"elements",
"displayMesh",
"displayValue",
"speckle_type",
"parameters",
"faces",
"colors",
"vertices",
"renderMaterial",
"textureCoordinates",
"totalChildrenCount"
}
DISPLAY_VALUE_PROPERTY_ALIASES = {"displayValue", "@displayValue"}
ELEMENTS_PROPERTY_ALIASES = {"elements", "@elements"}
OBJECT_NAME_MAX_LENGTH = 62
SPECKLE_ID_LENGTH = 32
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
OBJECT_NAME_NUMERAL_SEPARATOR = '.'
+811
View File
@@ -0,0 +1,811 @@
import math
from typing import Any, Dict, Iterable, List, Optional, Sequence, Union, Collection, cast
from bpy_speckle.convert.constants import DISPLAY_VALUE_PROPERTY_ALIASES, ELEMENTS_PROPERTY_ALIASES, OBJECT_NAME_MAX_LENGTH, OBJECT_NAME_NUMERAL_SEPARATOR, OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.functions import get_default_traversal_func, get_scale_length, _report
from bpy_speckle.convert.util import ConversionSkippedException
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
Quaternion as MQuaternion,
)
import bpy, bmesh
from specklepy.objects.other import (
Collection as SCollection,
Instance,
Transform,
BlockDefinition,
)
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh, Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle, Plane
from bpy.types import Object, Collection as BCollection
from .util import (
add_to_hierarchy,
get_render_material,
get_vertex_color_material,
render_material_to_native,
add_custom_properties,
add_vertices,
add_faces,
add_colors,
add_uv_coords,
)
SUPPORTED_CURVES = (Line, Polyline, Curve, Arc, Polycurve, Ellipse, Circle)
CAN_CONVERT_TO_NATIVE = (
Mesh,
*SUPPORTED_CURVES,
Instance,
)
def _has_native_conversion(speckle_object: Base) -> bool:
return any(isinstance(speckle_object, t) for t in CAN_CONVERT_TO_NATIVE) or "View" in speckle_object.speckle_type #hack
def _has_fallback_conversion(speckle_object: Base) -> bool:
return any(getattr(speckle_object, alias, None) for alias in DISPLAY_VALUE_PROPERTY_ALIASES)
def can_convert_to_native(speckle_object: Base) -> bool:
if(_has_native_conversion(speckle_object) or _has_fallback_conversion(speckle_object)):
return True
return False
convert_instances_as: str = "" #HACK: This is hacky, we need a better way to pass settings down to the converter
def set_convert_instances_as(value: str):
global convert_instances_as
convert_instances_as = value
#TODO: Check usages handle exceptions
def convert_to_native(speckle_object: Base) -> Object:
speckle_type = type(speckle_object)
object_name = _generate_object_name(speckle_object)
scale = get_scale_factor(speckle_object)
converted: Union[bpy.types.ID, bpy.types.Object, None] = None
children: list[Object] = []
# convert elements/breps
if not _has_native_conversion(speckle_object):
(converted, children) = display_value_to_native(speckle_object, object_name, scale)
if not converted and not children:
raise Exception(f"Zero geometry converted from displayValues for {speckle_object}")
# convert supported geometry
elif isinstance(speckle_object, Mesh):
converted = mesh_to_native(speckle_object, object_name, scale)
elif speckle_type in SUPPORTED_CURVES:
converted = icurve_to_native(speckle_object, object_name, scale)
elif "View" in speckle_object.speckle_type:
return view_to_native(speckle_object, object_name, scale)
elif isinstance(speckle_object, Instance):
if convert_instances_as == "linked_duplicates":
converted = instance_to_native_object(speckle_object, scale)
elif convert_instances_as == "collection_instance":
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
_report(f"convert_instances_as = '{convert_instances_as}' is not implemented, Instances will be converted as collection instances!")
converted = instance_to_native_collection_instance(speckle_object, scale)
else:
raise Exception(f"Unsupported type {speckle_type}")
if not isinstance(converted, Object):
converted = create_new_object(converted, object_name)
converted.speckle.object_id = str(speckle_object.id) # type: ignore
converted.speckle.enabled = True # type: ignore
add_custom_properties(speckle_object, converted)
for c in children:
c.parent = converted
return converted
def display_value_to_native(speckle_object: Base, name: str, scale: float) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
return _members_to_native(speckle_object, name, scale, DISPLAY_VALUE_PROPERTY_ALIASES, True)
def elements_to_native(speckle_object: Base, name: str, scale: float) -> list[bpy.types.Object]:
(_, elements) = _members_to_native(speckle_object, name, scale, ELEMENTS_PROPERTY_ALIASES, False)
return elements
def _members_to_native(speckle_object: Base, name: str, scale: float, members: Iterable[str], combineMeshes: bool) -> tuple[Optional[bpy.types.Mesh], list[bpy.types.Object]]:
"""
Converts a given speckle_object by converting specified members
if combineMeshes == True
Converts mesh members as one mesh
Converts non-mesh members as child Objects
if combineMeshes == False
Converts all members as child objects (first item of the returned tuple will be None)
:returns: converted mesh, and any other converted child objects (may happen if members contained non-meshes)
"""
meshes: list[Mesh] = []
others: list[Base] = []
for alias in members:
display = getattr(speckle_object, alias, None)
count = 0
MAX_DEPTH = 255 # some large value, to prevent infinite recursion
def separate(value: Any) -> bool:
nonlocal meshes, others, count, MAX_DEPTH
if combineMeshes and isinstance(value, Mesh):
meshes.append(value)
elif isinstance(value, Base):
others.append(value)
elif isinstance(value, list):
count += 1
if(count > MAX_DEPTH):
return True
for x in value:
separate(x)
return False
did_halt = separate(display)
if did_halt:
_report(f"Traversal of {speckle_object.speckle_type} {speckle_object.id} halted after traversal depth exceeds MAX_DEPTH={MAX_DEPTH}. Are there circular references object structure?")
children: list[Object] = []
mesh = None
if meshes:
mesh = meshes_to_native(speckle_object, meshes, name, scale) #TODO: reconsider passing scale around...
for item in others:
try:
blender_object = convert_to_native(item)
children.append(blender_object)
except Exception as ex:
_report(f"Failed to convert display value {item}: {ex}")
return (mesh, children)
def view_to_native(speckle_view, name: str, scale: float) -> bpy.types.Object:
native_cam: bpy.types.Camera
if name in bpy.data.cameras.keys():
native_cam = bpy.data.cameras[name]
else:
native_cam = bpy.data.cameras.new(name=name)
native_cam.lens = 18 # 90° horizontal fov
if not hasattr(speckle_view, "origin"):
raise ConversionSkippedException("2D views not supported")
cam_obj = create_new_object(native_cam, name)
scale_factor = get_scale_factor(speckle_view, scale)
tx = (speckle_view.origin.x * scale_factor)
ty = (speckle_view.origin.y * scale_factor)
tz = (speckle_view.origin.z * scale_factor)
forward = MVector((speckle_view.forwardDirection.x, speckle_view.forwardDirection.y, speckle_view.forwardDirection.z))
up = MVector((speckle_view.upDirection.x, speckle_view.upDirection.y, speckle_view.upDirection.z))
right = forward.cross(up).normalized()
cam_obj.matrix_world = MMatrix((
(right.x, up.x, -forward.x, tx),
(right.y, up.y, -forward.y, ty),
(right.z, up.z, -forward.z, tz),
(0, 0, 0, 1 )
))
return cam_obj
def mesh_to_native(speckle_mesh: Mesh, name: str, scale: float) -> bpy.types.Mesh:
return meshes_to_native(speckle_mesh, [speckle_mesh], name, scale)
def meshes_to_native(element: Base, meshes: Collection[Mesh], name: str, scale: float) -> bpy.types.Mesh:
if name in bpy.data.meshes.keys():
return bpy.data.meshes[name]
blender_mesh = bpy.data.meshes.new(name=name)
fallback_material = get_render_material(element)
bm = bmesh.new()
# First pass, add vertex data
for mesh in meshes:
scale = get_scale_factor(mesh, scale)
add_vertices(mesh, bm, scale)
bm.verts.ensure_lookup_table()
# Second pass, add face data
offset = 0
for i, mesh in enumerate(meshes):
if not mesh.vertices: continue
add_faces(mesh, bm, offset, i)
try:
render_material = get_render_material(mesh) or fallback_material
if render_material is not None:
native_material = render_material_to_native(render_material)
blender_mesh.materials.append(native_material)
elif mesh.colors:
native_material = get_vertex_color_material()
blender_mesh.materials.append(native_material)
except Exception as ex:
_report(f"Failed converting render material for {name}: {ex}")
offset += len(mesh.vertices) // 3
bm.faces.ensure_lookup_table()
bm.verts.index_update()
# Third pass, add vertex instance data
for mesh in meshes:
try:
add_colors(mesh, bm)
except Exception as ex:
_report(f"Skipping converting vertex colors for {name}: {ex}")
try:
add_uv_coords(mesh, bm)
except Exception as ex:
_report(f"Skipping converting uv coordinates for {name}: {ex}")
bm.to_mesh(blender_mesh)
bm.free()
return blender_mesh
"""
Curves
"""
def line_to_native(speckle_curve: Line, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not speckle_curve.end: return []
line = blender_curve.splines.new("POLY")
line.points.add(1)
line.points[0].co = (
float(speckle_curve.start.x) * scale,
float(speckle_curve.start.y) * scale,
float(speckle_curve.start.z) * scale,
1,
)
line.points[1].co = (
float(speckle_curve.end.x) * scale,
float(speckle_curve.end.y) * scale,
float(speckle_curve.end.z) * scale,
1,
)
return [line]
def polyline_to_native(scurve: Polyline, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not (value := scurve.value): return []
N = len(value) // 3
polyline = bcurve.splines.new("POLY")
if hasattr(scurve, "closed"):
polyline.use_cyclic_u = scurve.closed or False
polyline.points.add(N - 1)
for i in range(N):
polyline.points[i].co = (
float(value[i * 3]) * scale,
float(value[i * 3 + 1]) * scale,
float(value[i * 3 + 2]) * scale,
1,
)
return [polyline]
def nurbs_to_native(scurve: Curve, bcurve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
if not (points := scurve.points): return []
if not scurve.degree: raise Exception("curve is missing degree")
if not scurve.weights: raise Exception("curve is missing weights")
# Closed curves from rhino will have n + degree points. We ignore the extras
num_points = len(points) // 3 - scurve.degree if (scurve.closed) else (
len(points) // 3)
nurbs = bcurve.splines.new("NURBS")
nurbs.use_cyclic_u = scurve.closed or False
nurbs.use_endpoint_u = not scurve.periodic
nurbs.points.add(num_points - 1)
use_weights = len(scurve.weights) >= num_points
for i in range(num_points):
nurbs.points[i].co = (
float(points[i * 3]) * scale,
float(points[i * 3 + 1]) * scale,
float(points[i * 3 + 2]) * scale,
1,
)
nurbs.points[i].weight = scurve.weights[i] if use_weights else 1
nurbs.order_u = scurve.degree + 1
return [nurbs]
def arc_to_native(rcurve: Arc, bcurve: bpy.types.Curve, scale: float) -> Optional[bpy.types.Spline]:
# TODO: improve Blender representation of arc - check autocad test stream
if not rcurve.radius: raise Exception("curve is missing radius")
if not rcurve.startAngle: raise Exception("curve is missing startAngle")
if not rcurve.endAngle: raise Exception("curve is missing endAngle")
plane = rcurve.plane
if not plane:
return None
normal = MVector([plane.normal.x, plane.normal.y, plane.normal.z])
radius = rcurve.radius * scale
startAngle = rcurve.startAngle
endAngle = rcurve.endAngle
startQuat = MQuaternion(normal, startAngle) # type: ignore
endQuat = MQuaternion(normal, endAngle) # type: ignore
# Get start and end vectors, centre point, angles, etc.
r1 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r1.rotate(startQuat)
r2 = MVector([plane.xdir.x, plane.xdir.y, plane.xdir.z])
r2.rotate(endQuat)
c = MVector([plane.origin.x, plane.origin.y, plane.origin.z]) * scale
spt = c + r1 * radius
ept = c + r2 * radius
angle = endAngle - startAngle
t1 = normal.cross(r1)
# Initialize arc data and calculate subdivisions
arc = bcurve.splines.new("NURBS")
arc.use_cyclic_u = False
Ndiv = max(int(math.floor(angle / 0.3)), 2)
step = angle / float(Ndiv)
stepQuat = MQuaternion(normal, step) # type: ignore
tan = math.tan(step / 2) * radius
arc.points.add(Ndiv + 1)
# Set start and end points
arc.points[0].co = (spt.x, spt.y, spt.z, 1)
arc.points[Ndiv + 1].co = (ept.x, ept.y, ept.z, 1)
# Set intermediate points
for i in range(Ndiv):
t1 = normal.cross(r1)
pt = c + r1 * radius + t1 * tan
arc.points[i + 1].co = (pt.x, pt.y, pt.z, 1)
r1.rotate(stepQuat)
# Set curve settings
arc.use_endpoint_u = True
arc.order_u = 3
return arc
def polycurve_to_native(scurve: Polycurve, bcurve: bpy.types.Curve, scale: float) -> list[bpy.types.Spline]:
"""
Convert Polycurve object
"""
if not scurve.segments: raise Exception("curve is missing segments")
curves = []
for seg in scurve.segments:
speckle_type = type(seg)
if speckle_type in SUPPORTED_CURVES:
curves.append(icurve_to_native_spline(seg, bcurve, scale))
else:
_report(f"Unsupported curve type: {speckle_type}")
return curves
def ellipse_to_native(ellipse: Union[Ellipse, Circle], bcurve: bpy.types.Curve, units_scale: float) -> List[bpy.types.Spline]:
if not ellipse.plane: raise Exception("curve is missing plane")
radX: float
radY: float
if isinstance(ellipse, Ellipse):
if not ellipse.firstRadius: raise Exception("curve is missing firstRadius")
if not ellipse.secondRadius: raise Exception("curve is missing secondRadius")
radX = ellipse.firstRadius * units_scale
radY = ellipse.secondRadius * units_scale
else:
if not ellipse.radius: raise Exception("curve is missing radius")
radX = ellipse.radius * units_scale
radY = ellipse.radius * units_scale
D = 0.5522847498307936 # (4/3)*tan(pi/8)
right_handles = [
(+radX, +radY * D, 0.0),
(-radX * D, +radY, 0.0),
(-radX, -radY * D, 0.0),
(+radX * D, -radY, 0.0),
]
left_handles = [
(+radX, -radY * D, 0.0),
(+radX * D, +radY, 0.0),
(-radX, +radY * D, 0.0),
(-radX * D, -radY, 0.0),
]
points = [
(+radX, 0.0, 0.0),
(0.0, +radY, 0.0),
(-radX, 0.0, 0.0),
(0.0, -radY, 0.0),
]
transform = plane_to_native_transform(ellipse.plane, units_scale)
spline = bcurve.splines.new("BEZIER")
spline.bezier_points.add(len(points) - 1)
for i in range(len(points)):
spline.bezier_points[i].co = transform @ MVector(points[i]) # type: ignore
spline.bezier_points[i].handle_left = transform @ MVector(left_handles[i]) # type: ignore
spline.bezier_points[i].handle_right = transform @ MVector(right_handles[i]) # type: ignore
spline.use_cyclic_u = True
#TODO support trims?
return [spline]
def icurve_to_native_spline(speckle_curve: Base, blender_curve: bpy.types.Curve, scale: float) -> List[bpy.types.Spline]:
# polycurves
if isinstance(speckle_curve, Polycurve):
return polycurve_to_native(speckle_curve, blender_curve, scale)
splines: List[bpy.types.Spline]
# single curves
if isinstance(speckle_curve, Line):
splines = line_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Curve):
splines = nurbs_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Polyline):
splines = polyline_to_native(speckle_curve, blender_curve, scale)
elif isinstance(speckle_curve, Arc):
spline = arc_to_native(speckle_curve, blender_curve, scale)
splines = [spline] if spline else []
elif isinstance(speckle_curve, Ellipse) or isinstance(speckle_curve, Circle):
splines = ellipse_to_native(speckle_curve, blender_curve, scale)
else:
raise TypeError(f"{speckle_curve} is not a supported curve type. Supported types: {SUPPORTED_CURVES}")
return splines
def icurve_to_native(speckle_curve: Base, name: str, scale: float) -> bpy.types.Curve:
curve_type = type(speckle_curve)
if curve_type not in SUPPORTED_CURVES:
raise Exception(f"Unsupported curve type: {curve_type}")
blender_curve = (
bpy.data.curves[name]
if name in bpy.data.curves.keys()
else bpy.data.curves.new(name, type="CURVE")
)
blender_curve.dimensions = "3D"
blender_curve.resolution_u = 12 #TODO: We could maybe decern the resolution from the polyline displayValue
icurve_to_native_spline(speckle_curve, blender_curve, scale)
return blender_curve
"""
Transforms and Instances
"""
def transform_to_native(transform: Transform, scale: float) -> MMatrix:
mat = MMatrix(
[
transform.value[:4],
transform.value[4:8],
transform.value[8:12],
transform.value[12:16],
]
)
# scale the translation
for i in range(3):
mat[i][3] *= scale
return mat
def plane_to_native_transform(plane: Plane, fallback_scale:float = 1) -> MMatrix:
scale_factor = get_scale_factor(plane, fallback_scale)
tx = (plane.origin.x * scale_factor)
ty = (plane.origin.y * scale_factor)
tz = (plane.origin.z * scale_factor)
return MMatrix((
(plane.xdir.x, plane.ydir.x, plane.normal.x, tx),
(plane.xdir.y, plane.ydir.y, plane.normal.y, ty),
(plane.xdir.z, plane.ydir.z, plane.normal.z, tz),
(0, 0, 0, 1 )
))
"""
Instances / Blocks
"""
def _get_instance_name(instance: Instance) -> str:
if not instance.definition: raise Exception("Instance is missing a definition")
name_prefix = (
_get_friendly_object_name(instance)
or _get_friendly_object_name(instance.definition)
or _simplified_speckle_type(instance.speckle_type)
)
return f"{name_prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{instance.id}"
def instance_to_native_object(instance: Instance, scale: float) -> Object:
"""
Converts Instance to a unique object with (potentially) shared data (linked duplicate)
"""
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.transform: raise Exception("Instance is missing a transform")
definition = instance.definition
if not definition.id: raise Exception("Instance is missing a valid definition")
name = _get_instance_name(instance)
native_instance: Optional[Object] = None
converted_objects: Dict[str, Union[Object, BCollection]] = {}
traversal_root: Base = definition
if not can_convert_to_native(definition):
# Non-convertible (like all blocks, and some revit instances) will not be converted as part of the deep_traversal.
# so we explicitly convert them as empties.
native_instance = create_new_object(None, name)
native_instance.empty_display_size = 0
converted_objects["__ROOT"] = native_instance # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
traversal_root = Base(elements=definition, id="__ROOT")
#Convert definition + "elements" on definition
_deep_conversion(traversal_root, converted_objects, False)
if not native_instance:
assert(can_convert_to_native(definition))
if not definition.id in converted_objects:
raise Exception("Definition was not converted")
converted = converted_objects[definition.id]
if not isinstance(converted, Object):
raise Exception("Definition was not converted to an Object")
native_instance = converted
instance_transform = transform_to_native(instance.transform, scale)
native_instance.matrix_world = instance_transform
return native_instance
def instance_to_native_collection_instance(instance: Instance, scale: float) -> bpy.types.Object:
"""
Convert an Instance as a transformed Object with the `instance_collection` property
set to be the `instance.Definition` converted as a collection
The definition collection won't be linked to the current scene
Any Elements on the instance object will also be converted (and spacially transformed)
"""
if not instance.definition: raise Exception("Instance is missing a definition")
if not instance.transform: raise Exception("Instance is missing a transform")
name = _get_instance_name(instance)
# Get/Convert definition collection
collection_def = _instance_definition_to_native(instance.definition)
instance_transform = transform_to_native(instance.transform, scale)
native_instance = create_new_object(None, name)
#add_custom_properties(instance, native_instance)
# hide the instance axes so they don't clutter the viewport
native_instance.empty_display_size = 0
native_instance.instance_collection = collection_def
native_instance.instance_type = "COLLECTION"
native_instance.matrix_world = instance_transform
return native_instance
def _instance_definition_to_native(definition: Union[Base, BlockDefinition]) -> bpy.types.Collection:
"""
Converts a geometry carrying Base as a collection (does not link it to the scene)
"""
name = _generate_object_name(definition)
native_def = bpy.data.collections.get(name)
if native_def:
return native_def
native_def = create_new_collection(name)
native_def["applicationId"] = definition.applicationId
converted_objects = {}
converted_objects["__ROOT"] = native_def # we create a dummy root to avoid id conflicts, since revit definitions have displayValues, they are convertible
dummyRoot = Base(elements=definition, id="__ROOT")
_deep_conversion(dummyRoot, converted_objects, True)
return native_def
def _deep_conversion(root: Base, converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool):
traversal_func = get_default_traversal_func(can_convert_to_native)
for item in traversal_func.traverse(root):
current: Base = item.current
if can_convert_to_native(current) or isinstance(current, SCollection):
try:
if not current or not current.id: raise Exception(f"{current} was an invalid speckle object")
#Convert the object!
converted_data_type: str
converted: Union[Object, BCollection, None]
if isinstance(current, SCollection):
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException()
converted = collection_to_native(current)
converted_data_type = "COLLECTION"
else:
converted = convert_to_native(current)
converted_data_type = "COLLECTION_INSTANCE" if converted.instance_collection else str(converted.type)
if converted is None:
raise Exception("Conversion returned None")
converted_objects[current.id] = converted
add_to_hierarchy(converted, item, converted_objects, preserve_transform)
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
except ConversionSkippedException as ex:
_report(f"Skipped converting {type(current).__name__} {current.id}: {ex}")
except Exception as ex:
_report(f"Failed to converted {type(current).__name__} {current.id}: {ex}")
def collection_to_native(collection: SCollection) -> BCollection:
name = collection.name or f"{collection.collectionType} -- {collection.applicationId or collection.id}" #TODO: consider consolidating name formatting with Rhino
ret = get_or_create_collection(name)
color = getattr(collection, "colorTag", None)
if color:
ret.color_tag = color
return ret
def get_or_create_collection(name: str, clear_collection: bool = True) -> BCollection:
#Disabled for now, since update mode needs rescoping.
# existing = cast(Optional[BCollection], bpy.data.collections.get(name))
# if existing:
# if clear_collection:
# for obj in existing.objects:
# existing.objects.unlink(obj)
# return existing
# else:
new_collection = create_new_collection(name)
#NOTE: We want to not render revit "Rooms" collections by default.
if name == "Rooms":
new_collection.hide_viewport = True
new_collection.hide_render = True
return new_collection
"""
Object Naming and Creation
"""
def create_new_collection( desired_name: str) -> bpy.types.Collection:
"""
Creates a new blender collection with a unique name
If the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = _make_unique_name(desired_name, bpy.data.collections.keys())
blender_collection = bpy.data.collections.new(name)
return blender_collection
def create_new_object(obj_data: Optional[bpy.types.ID], desired_name: str) -> bpy.types.Object:
"""
Creates a new blender object with a unique name,
If the desired_name is already taken
we'll append a number, with the format .xxx to the desired_name to ensure the name is unique.
"""
name = _make_unique_name(desired_name, bpy.data.objects.keys())
blender_object = bpy.data.objects.new(name, obj_data)
return blender_object
def _make_unique_name( desired_name: str, taken_names: Collection[str], counter: int = 0) -> str:
"""
Using Blenders default naming (append numeral in .xxx format) to avoid name conflicts with taken names
"""
name = desired_name if counter == 0 else f"{desired_name[:OBJECT_NAME_MAX_LENGTH - 4]}{OBJECT_NAME_NUMERAL_SEPARATOR}{counter:03d}" # format counter as name.xxx, truncate to ensure we don't exceed the object name max length
#TODO: This is very slow, and gets slower the more objects you receive with the same name...
# We could use a binary/galloping search, and/or cache the name -> index within a receive.
if name in taken_names:
#Name already taken, increment counter and try again!
return _make_unique_name(desired_name, taken_names, counter + 1)
return name
def _get_friendly_object_name(speckle_object: Base) -> Optional[str]:
return (getattr(speckle_object, "name", None)
or getattr(speckle_object, "Name", None)
or getattr(speckle_object, "family", None)
)
# Blender object names must not exceed 62 characters
# We need to ensure the complete ID is included in the name (to prevent identity collisions)
# So we if the name is too long, we need to truncate
def _truncate_object_name(name: str) -> str:
MAX_NAME_LENGTH = OBJECT_NAME_MAX_LENGTH - SPECKLE_ID_LENGTH - len(OBJECT_NAME_SPECKLE_SEPARATOR)
return name[:MAX_NAME_LENGTH]
def _simplified_speckle_type(speckle_type: str) -> str:
return(speckle_type.rsplit('.')[-1]) #Take only the most specific object type name (without namespace)
def _generate_object_name(speckle_object: Base) -> str:
prefix: str
name = _get_friendly_object_name(speckle_object)
if name:
prefix = _truncate_object_name(name)
else:
prefix = _simplified_speckle_type(speckle_object.speckle_type)
return f"{prefix}{OBJECT_NAME_SPECKLE_SEPARATOR}{speckle_object.id}"
def get_scale_factor(speckle_object: Base, fallback: float = 1.0) -> float:
scale = fallback
if units := getattr(speckle_object, "units", None):
scale = get_scale_length(units) / bpy.context.scene.unit_settings.scale_length
return scale
+541
View File
@@ -0,0 +1,541 @@
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
import bpy
from bpy.types import (
Depsgraph,
MeshPolygon,
Object,
Curve as NCurve,
Mesh as NMesh,
Camera as NCamera,
)
from deprecated import deprecated
from mathutils.geometry import interpolate_bezier
from mathutils import (
Matrix as MMatrix,
Vector as MVector,
)
from specklepy.objects import Base
from specklepy.objects.other import BlockInstance, BlockDefinition, RenderMaterial, Transform
from specklepy.objects.geometry import (
Mesh, Curve, Interval, Box, Point, Vector, Polyline,
)
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.constants import OBJECT_NAME_SPECKLE_SEPARATOR, SPECKLE_ID_LENGTH
from bpy_speckle.convert.util import (
ConversionSkippedException,
get_blender_custom_properties,
make_knots,
nurb_make_curve,
to_argb_int,
)
from bpy_speckle.functions import _report
Units: str = "m" # The desired final units to send
UnitsScale: float = 1 # The scale factor conversions need to apply to position data to get to the desired units
CAN_CONVERT_TO_SPECKLE = ("MESH", "CURVE", "EMPTY", "CAMERA", "FONT", "SURFACE", "META")
def convert_to_speckle(raw_blender_object: Object, units_scale: float, units: str, depsgraph: Optional[Depsgraph]) -> Base:
"""
Converts supported 1 blender objects to 1 speckle object (potentially with children)
:param raw_blender_object: the blender object (unevaluated by a Depsgraph) to convert
:param units_scale: The scale factor conversions need to apply to position data to get to the desired units
:param units: The desired final units to send
:param depsgraph: Optional depsgraph if provided will evaluate modifiers on geometry data
:return: The Converted blender object
"""
global Units, UnitsScale
Units = units
UnitsScale = units_scale
blender_type = raw_blender_object.type
if blender_type not in CAN_CONVERT_TO_SPECKLE:
raise ConversionSkippedException(f"Objects of type {blender_type} are not supported")
blender_object = cast(Object, (
raw_blender_object.evaluated_get(depsgraph)
if depsgraph
else raw_blender_object
))
converted: Optional[Base] = None
if blender_type == "MESH":
converted = mesh_to_speckle(blender_object, cast(NMesh, blender_object.data))
elif blender_type == "CURVE":
converted = curve_to_speckle(blender_object, cast(NCurve, blender_object.data))
elif blender_type == "EMPTY":
converted = empty_to_speckle(blender_object)
elif blender_type == "CAMERA":
converted = camera_to_speckle_view(blender_object, cast(NCamera, blender_object.data))
elif blender_type == "FONT" or "SURFACE" or "META":
converted = anything_to_speckle_mesh(blender_object)
if not converted:
raise Exception("Conversion returned None")
converted["properties"] = get_blender_custom_properties(raw_blender_object) #NOTE: Depsgraph copies don't have custom properties so we use the raw version
# Set object transform #TODO: this could be deprecated once we add proper geometry instancing support
if blender_type != "EMPTY":
converted["properties"]["transform"] = transform_to_speckle(
blender_object.matrix_world
)
return converted
def mesh_to_speckle(blender_object: Object, data: bpy.types.Mesh) -> Base:
b = Base()
b["name"] = to_speckle_name(blender_object)
b["@displayValue"] = mesh_to_speckle_meshes(blender_object, data)
return b
def mesh_to_speckle_meshes(blender_object: Object, data: bpy.types.Mesh) -> List[Mesh]:
# Categorise polygons by material index
submesh_data: Dict[int, List[MeshPolygon]] = {}
for p in data.polygons:
if p.material_index not in submesh_data:
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world)
scaled_vertices = [tuple(transform @ x.co * UnitsScale) for x in data.vertices]
# Create Speckle meshes for each material
submeshes = []
index_counter = 0
for i in submesh_data:
index_mapping: Dict[int, int] = {}
#Loop through each polygon, and map indices to their new index in m_verts
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
for face in submesh_data[i]:
u_indices = face.vertices
m_faces.append(len(u_indices))
mesh_area += face.area
for u_index in u_indices:
if u_index not in index_mapping:
# Create mapping between index in blender mesh, and new index in speckle submesh
index_mapping[u_index] = len(m_verts) // 3
vert = scaled_vertices[u_index]
m_verts.append(vert[0])
m_verts.append(vert[1])
m_verts.append(vert[2])
if data.uv_layers.active:
vt = data.uv_layers.active.data[index_counter]
uv = cast(MVector, vt.uv)
m_texcoords.extend([uv.x, uv.y])
m_faces.append(index_mapping[u_index])
index_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
units=Units,
area = mesh_area,
bbox=Box(area=0.0, volume=0.0),
)
if i < len(data.materials):
material = data.materials[i]
if material is not None:
speckle_mesh["renderMaterial"] = material_to_speckle(material)
submeshes.append(speckle_mesh)
return submeshes
def bezier_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points: List[Tuple[MVector]] = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
points.append(tuple(matrix @ bp.handle_left * UnitsScale)) # type: ignore
points.append(tuple(matrix @ bp.co * UnitsScale)) # type: ignore
if i < len(spline.bezier_points) - 1:
points.append(tuple(matrix @ bp.handle_right * UnitsScale)) # type: ignore
if closed:
points.extend(
(
tuple(matrix @ spline.bezier_points[-1].handle_right * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].handle_left * UnitsScale), # type: ignore
tuple(matrix @ spline.bezier_points[0].co * UnitsScale), # type: ignore
)
)
num_points = len(points)
flattened_points = []
for row in points: flattened_points.extend(row)
knot_count = num_points + degree - 1
knots = [0] * knot_count
for i in range(1, len(knots)):
knots[i] = i // 3
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
rational=True,
area=0,
volume=0,
length=length,
domain=domain,
units=Units,
bbox=Box(area=0.0, volume=0.0),
displayValue = bezier_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
weights = [pt.weight for pt in spline.points]
is_rational = all(w == weights[0] for w in weights)
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattened_points = []
for row in points: flattened_points.extend(row)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
# Rhino expects n + degree number of points (for closed curves). So we need to add an extra point for each degree
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
return Curve(
name=name,
degree=degree,
closed=spline.use_cyclic_u,
periodic= not spline.use_endpoint_u,
points=flattened_points,
weights=weights,
knots=knots,
rational=is_rational,
area=0,
volume=0,
length=length,
domain=domain,
units=Units,
bbox=Box(area=0.0, volume=0.0),
displayValue=nurbs_to_speckle_polyline(matrix, spline, length),
)
def nurbs_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Polyline:
"""
Samples a nurbs curve with resolution_u creating a polyline
"""
points: List[float] = []
sampled_points = nurb_make_curve(spline, spline.resolution_u, 3)
for i in range(0, len(sampled_points), 3):
scaled_point = cast(Vector, matrix @ MVector((
sampled_points[i + 0],
sampled_points[i + 1],
sampled_points[i + 2])) * UnitsScale)
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
#Inspired by https://blender.stackexchange.com/a/689 (CC BY-SA 3.0)
def bezier_to_speckle_polyline(matrix: MMatrix, spline: bpy.types.Spline, length: Optional[float] = None) -> Optional[Polyline]:
"""
Samples a Bézier curve with resolution_u creating a polyline
"""
segments = len(spline.bezier_points)
if segments < 2: return None
R = spline.resolution_u + 1
points = []
if not spline.use_cyclic_u:
segments -= 1
points: List[float] = []
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
_points = interpolate_bezier(knot1, handle1, handle2, knot2, R)
for p in _points:
scaled_point = matrix @ p * UnitsScale
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(value=points, closed = spline.use_cyclic_u, domain=domain, area=0, len=length)
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = len(blender_object.name) > _QUICK_TEST_NAME_LENGTH and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name
def poly_to_speckle(matrix: MMatrix, spline: bpy.types.Spline, name: Optional[str] = None) -> Polyline:
points = [tuple(matrix @ pt.co.xyz * UnitsScale) for pt in spline.points] # type: ignore
flattened_points = []
for row in points: flattened_points.extend(row)
length = spline.calc_length()
domain = Interval(start=0, end=length, totalChildrenCount=0)
return Polyline(
name=name,
closed=bool(spline.use_cyclic_u),
value=list(flattened_points),
length=length,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=Units,
)
def curve_to_speckle(blender_object: Object, data: bpy.types.Curve) -> Base:
b = Base()
(meshes, curves) = curve_to_speckle_geometry(blender_object, data)
if meshes:
b["@displayValue"] = meshes
b["name"] = to_speckle_name(blender_object)
b["@elements"] = curves
return b
def curve_to_speckle_geometry(blender_object: Object, data: bpy.types.Curve) -> Tuple[List[Mesh], List[Base]]:
assert(blender_object.type == "CURVE")
blender_object = cast(Object, blender_object.evaluated_get(bpy.context.view_layer.depsgraph))
matrix = cast(MMatrix, blender_object.matrix_world)
meshes: List[Mesh] = []
curves: List[Base] = []
#TODO: Could we support this better?
if data.bevel_mode == "OBJECT" and data.bevel_object != None:
meshes = mesh_to_speckle_meshes(blender_object, blender_object.to_mesh())
for spline in data.splines:
if spline.type == "BEZIER":
curves.append(bezier_to_speckle(matrix, spline, to_speckle_name(blender_object)))
elif spline.type == "NURBS":
curves.append(nurbs_to_speckle(matrix, spline, to_speckle_name(blender_object)))
elif spline.type == "POLY":
curves.append(poly_to_speckle(matrix, spline, to_speckle_name(blender_object)))
return (meshes, curves)
def anything_to_speckle_mesh(blender_object: Object) -> Base:
mesh = mesh_to_speckle(blender_object, blender_object.to_mesh())
blender_object.to_mesh_clear()
return mesh
@deprecated
def ngons_to_speckle_polylines(blender_object: Object, data: bpy.types.Mesh) -> Optional[List[Polyline]]:
UNITS = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
if blender_object.type != "MESH":
return None
mat = blender_object.matrix_world
verts = data.vertices
polylines = []
for i, poly in enumerate(data.polygons):
value = []
for v in poly.vertices:
value.extend(mat @ verts[v].co * UnitsScale) # type: ignore
domain = Interval(start=0, end=1)
poly = Polyline(
name="{}_{}".format(blender_object.name, i),
closed=True,
value=value,
length=0,
domain=domain,
bbox=Box(area=0.0, volume=0.0),
area=0,
units=UNITS,
)
polylines.append(poly)
return polylines
def material_to_speckle(blender_mat: bpy.types.Material) -> RenderMaterial:
speckle_mat = RenderMaterial()
speckle_mat.name = blender_mat.name
if blender_mat.use_nodes:
if blender_mat.node_tree.nodes.get("Principled BSDF"):
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
speckle_mat.diffuse = to_argb_int(inputs["Base Color"].default_value) # type: ignore
speckle_mat.emissive = to_argb_int(inputs[emission_color].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
speckle_mat.metalness = inputs["Metallic"].default_value # type: ignore
speckle_mat.opacity = inputs["Alpha"].default_value # type: ignore
return speckle_mat
elif blender_mat.node_tree.nodes.get("Diffuse BSDF"):
inputs = blender_mat.node_tree.nodes["Diffuse BSDF"].inputs
speckle_mat.diffuse = to_argb_int(inputs["Color"].default_value) # type: ignore
speckle_mat.roughness = inputs["Roughness"].default_value # type: ignore
return speckle_mat
#TODO: Support more shaders
# fallback to standard material props
speckle_mat.diffuse = to_argb_int(blender_mat.diffuse_color) # type: ignore
speckle_mat.metalness = blender_mat.metallic
speckle_mat.roughness = blender_mat.roughness
return speckle_mat
def camera_to_speckle_view(blender_object: Object, data: NCamera) -> Base:
if data.type != 'PERSP':
raise Exception(f"Cameras of type {data.type} are not currently supported")
matrix = cast(MMatrix, blender_object.matrix_world)
up = cast(MVector, matrix.col[1].xyz)
forwards = cast(MVector, -matrix.col[2].xyz)
translation = matrix.translation
view = Base.of_type("Objects.BuiltElements.View:Objects.BuiltElements.View3D") #HACK: views are not in specklepy yet!
view.name = to_speckle_name(blender_object)
view.origin = vector_to_speckle_point(translation)
view.upDirection = vector_to_speckle(up)
view.forwardDirection = vector_to_speckle(forwards)
view.target = vector_to_speckle_point(forwards) #TODO: do these need to be scaled?
view.units = Units
view.isOrthogonal = False
return view
def vector_to_speckle_point(xyz: MVector) -> Point:
return Point(
x = xyz.x * UnitsScale,
y = xyz.y * UnitsScale,
z = xyz.z * UnitsScale,
units = Units,
)
def vector_to_speckle(xyz: MVector) -> Vector:
return Vector(
x = xyz.x * UnitsScale,
y = xyz.y * UnitsScale,
z = xyz.z * UnitsScale,
units = Units,
)
def transform_to_speckle(blender_transform: Union[Iterable[Iterable[float]], MMatrix]) -> Transform:
iterable_transform = cast(Iterable[Iterable[float]], blender_transform) #NOTE: Matrix are iterable, even if type hinting says they are not
value = [y for x in iterable_transform for y in x]
# scale the translation
for i in (3, 7, 11):
value[i] *= UnitsScale
return Transform(value=value, units=Units)
def block_def_to_speckle(blender_definition: bpy.types.Collection) -> BlockDefinition:
geometryBuilder = BlenderCommitObjectBuilder()
for geo in blender_definition.objects:
try:
c = convert_to_speckle(geo, UnitsScale, Units, None)
geometryBuilder.include_object(c, geo)
except ConversionSkippedException as ex:
_report(f"Skipped converting '{geo.name_full}' inside collection instance: '{ex}")
except Exception as ex:
_report(f"Failed to converted '{geo.name_full}' inside collection instance: '{ex}'")
dummyRoot = Base()
geometryBuilder.apply_relationships(geometryBuilder.converted.values(), dummyRoot)
block_def = BlockDefinition(
units=Units,
name=to_speckle_name(blender_definition),
geometry=dummyRoot["@elements"],
basePoint=Point(units=Units),
)
# blender_props = get_blender_custom_properties(blender_definition)
# block_def.applicationId = blender_props.pop("applicationId", None) #TODO: remove?
return block_def
def block_instance_to_speckle(blender_instance: Object) -> BlockInstance:
return BlockInstance(
blockDefinition=block_def_to_speckle(
blender_instance.instance_collection
),
transform=transform_to_speckle(blender_instance.matrix_world),
name=to_speckle_name(blender_instance),
units=Units,
)
def empty_to_speckle(blender_object: Object) -> Union[BlockInstance, Base]:
# probably an instance collection (block) so let's try it
if blender_object.instance_collection and blender_object.instance_type == "COLLECTION":
# Empty -> Block
return block_instance_to_speckle(blender_object)
else:
# Empty -> Point
wrapper = Base()
wrapper["@displayValue"] = matrix_to_speckle_point(cast(MMatrix, blender_object.matrix_world))
return wrapper
def matrix_to_speckle_point(matrix: MMatrix, units_scale: float = 1.0) -> Point:
transformed_pos = cast(MVector, matrix @ MVector((0,0,0)) * units_scale)
return Point(x = transformed_pos.x,
y = transformed_pos.y,
z = transformed_pos.z)
+493
View File
@@ -0,0 +1,493 @@
import math
from typing import Any, Dict, Optional, Tuple, Union, cast
from bmesh.types import BMesh
import bpy, idprop
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh
from specklepy.objects.other import RenderMaterial
from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS
from bpy_speckle.functions import _report
from bpy.types import Material, Object, Collection as BCollection, Node, ShaderNodeVertexColor, NodeInputs
from specklepy.objects.graph_traversal.traversal import TraversalContext
class ConversionSkippedException(Exception):
pass
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
"""Converts the int representation of a colour into a percent RGBA tuple"""
alpha = ((argb_int >> 24) & 255) / 255
red = ((argb_int >> 16) & 255) / 255
green = ((argb_int >> 8) & 255) / 255
blue = (argb_int & 255) / 255
return (red, green, blue, alpha)
def to_argb_int(rgba_color: list[float]) -> int:
"""Converts an RGBA array to an ARGB integer"""
argb_color = rgba_color[-1:] + rgba_color[:3]
int_color = [int(val * 255) for val in argb_color]
return int.from_bytes(int_color, byteorder="big", signed=True)
def set_custom_property(key: str, value: Any, blender_object: Object) -> None:
try:
#Expected c types: float, int, string, float[], int[]
blender_object[key] = value
except (OverflowError, TypeError) as ex:
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
except Exception as ex:
#TODO: Log this as it's unexpected!!!
print(f"Skipping setting property ({key}={value}) on {blender_object.name_full}, Reason: {ex}")
def add_custom_properties(speckle_object: Base, blender_object: Object):
if blender_object is None:
return
blender_object["_speckle_type"] = type(speckle_object).__name__
app_id = getattr(speckle_object, "applicationId", None)
if app_id:
blender_object["applicationId"] = speckle_object.applicationId
keys = speckle_object.get_dynamic_member_names() if "Geometry" in speckle_object.speckle_type else (set(speckle_object.get_member_names()) - IGNORED_PROPERTY_KEYS)
for key in keys:
val = getattr(speckle_object, key, None)
if val is None:
continue
if isinstance(val, (int, str, float)):
set_custom_property(key, val, blender_object)
elif key == "properties" and isinstance(val, Base):
val["applicationId"] = None
add_custom_properties(val, blender_object)
elif isinstance(val, list):
items = [item for item in val if not isinstance(item, Base)]
if items:
set_custom_property(key, items, blender_object)
elif isinstance(val,dict):
for (k,v) in val.items():
if not isinstance(v, Base):
set_custom_property(k, v, blender_object)
def render_material_to_native(speckle_mat: RenderMaterial) -> Material:
mat_name = speckle_mat.name
if not mat_name:
mat_name = speckle_mat.applicationId or speckle_mat.id or speckle_mat.get_id()
blender_mat = bpy.data.materials.get(mat_name)
if blender_mat is None:
blender_mat = bpy.data.materials.new(mat_name)
# for now, we're not updating these materials. as per tom's suggestion, we should have a toggle
# that enables this as the blender mats will prob be much more complex than whatever is coming in
blender_mat.use_nodes = True
inputs = blender_mat.node_tree.nodes["Principled BSDF"].inputs
inputs["Base Color"].default_value = to_rgba(speckle_mat.diffuse) # type: ignore
inputs["Roughness"].default_value = speckle_mat.roughness # type: ignore
inputs["Metallic"].default_value = speckle_mat.metalness # type: ignore
inputs["Alpha"].default_value = speckle_mat.opacity # type: ignore
# Blender >=4.0 use "Emission Color"
emission_color = "Emission" if "Emission" in inputs else "Emission Color" # type: ignore
inputs[emission_color].default_value = to_rgba(speckle_mat.emissive) # type: ignore
if speckle_mat.opacity < 1.0:
blender_mat.blend_method = "BLEND"
return blender_mat
_vertex_color_material: Optional[Material] = None
def get_vertex_color_material() -> Material:
global _vertex_color_material
#see https://stackoverflow.com/a/69807985
if not _vertex_color_material:
_vertex_color_material = bpy.data.materials.new("Vertex Color Material")
_vertex_color_material.use_nodes = True
nodes = _vertex_color_material.node_tree.nodes
principled_bsdf_node = cast(Node, nodes.get("Principled BSDF"))
if not "VERTEX_COLOR" in [node.type for node in nodes]:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.new(type = "ShaderNodeVertexColor"))
else:
vertex_color_node = cast(ShaderNodeVertexColor, nodes.get("Vertex Color"))
vertex_color_node.layer_name = "Col"
links = _vertex_color_material.node_tree.links
link = links.new(vertex_color_node.outputs[0], principled_bsdf_node.inputs[0])
return _vertex_color_material
def get_render_material(speckle_object: Base) -> Optional[RenderMaterial]:
"""Trys to get a RenderMaterial on given speckle_object"""
speckle_mat = getattr(
speckle_object,
"renderMaterial",
getattr(speckle_object, "@renderMaterial", None),
)
if isinstance(speckle_mat, RenderMaterial):
return speckle_mat
return None
def add_vertices(speckle_mesh: Mesh, blender_mesh: BMesh, scale=1.0):
sverts = speckle_mesh.vertices
if sverts and len(sverts) > 0:
for i in range(0, len(sverts), 3):
blender_mesh.verts.new(
(
float(sverts[i]) * scale,
float(sverts[i + 1]) * scale,
float(sverts[i + 2]) * scale,
)
)
def add_faces(speckle_mesh: Mesh, blender_mesh: BMesh, indexOffset: int, materialIndex: int = 0, smooth:bool = True):
sfaces = speckle_mesh.faces
if sfaces and len(sfaces) > 0:
i = 0
while i < len(sfaces):
n = sfaces[i]
if n < 3:
n += 3 # 0 -> 3, 1 -> 4
i += 1
try:
f = blender_mesh.faces.new(
[blender_mesh.verts[x + indexOffset] for x in sfaces[i : i + n]]
)
f.material_index = materialIndex
f.smooth = smooth
except Exception as e:
_report(f"Failed to create face for mesh {speckle_mesh.id} \n{e}")
i += n
def add_colors(speckle_mesh: Mesh, blender_mesh: BMesh):
scolors = speckle_mesh.colors
if scolors:
colors = []
if len(scolors) > 0:
for i in range(len(scolors)):
argb = int(scolors[i])
(a, r, g, b) = argb_split(argb)
colors.append(
(
float(r) / 255.0,
float(g) / 255.0,
float(b) / 255.0,
float(a) / 255.0,
)
)
# Make vertex colors
if len(scolors) == len(blender_mesh.verts):
color_layer = blender_mesh.loops.layers.color.new("Col")
for face in blender_mesh.faces:
for loop in face.loops:
loop[color_layer] = colors[loop.vert.index]
def argb_split(argb: int) -> Tuple[int, int, int, int]:
alpha = (argb >> 24) & 0xFF
red = (argb >> 16) & 0xFF
green = (argb >> 8) & 0xFF
blue = argb & 0xFF
return (alpha, red, green, blue)
def add_uv_coords(speckle_mesh: Mesh, blender_mesh: BMesh):
s_uvs = speckle_mesh.textureCoordinates
if not s_uvs:
return
try:
uv = []
if len(s_uvs) // 2 == len(blender_mesh.verts):
uv.extend(
(float(s_uvs[i]), float(s_uvs[i + 1]))
for i in range(0, len(s_uvs), 2)
)
else:
_report(
f"Failed to match UV coordinates to vert data. Blender mesh verts: {len(blender_mesh.verts)}, Speckle UVs: {len(s_uvs) // 2}"
)
return
# Make UVs
uv_layer = blender_mesh.loops.layers.uv.verify()
for f in blender_mesh.faces:
for l in f.loops:
luv = l[uv_layer]
luv.uv = uv[l.vert.index]
except:
_report("Failed to decode texture coordinates.")
raise
ignored_keys = {
"id",
"speckle",
"speckle_type"
"_speckle_type",
"_speckle_name",
"_speckle_transform",
"_RNA_UI",
"elements",
"transform",
"_units",
"_chunkable",
}
def get_blender_custom_properties(obj, max_depth: int = 63):
"""Recursively grabs custom properties on blender objects. Max depth is determined by the max allowed by Newtonsoft.NET, don't exceed unless you know what you're doing"""
if max_depth <= 0:
return obj
if hasattr(obj, "keys"):
keys = set(obj.keys()) - ignored_keys
return {
key: get_blender_custom_properties(obj[key], max_depth - 1)
for key in keys
if not key.startswith("_")
}
if isinstance(obj, (list, tuple, idprop.types.IDPropertyArray)):
return [get_blender_custom_properties(o, max_depth - 1) for o in obj] # type: ignore
return obj
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * macro_knotsu(nu)
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order
if flag == 1: # CU_NURB_ENDPOINT
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2: # CU_NURB_BEZIER
if order == 4:
k = 0.34
for a in range(pts_order):
knots[a] = math.floor(k)
k += 1.0 / 3.0
elif order == 3:
k = 0.6
for a in range(pts_order):
if a >= order and a <= point_count:
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(1, len(knots) - 1):
knots[a] = a - 1
knots[-1] = knots[-2]
def basis_nurb(t: float, order: int, point_count: int, knots: list[float], basis: list[float], start: int, end: int) -> Tuple[int, int]:
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
# this part is order '1'
o2 = order + 1
for i in range(opp2):
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
basis[i] = 1.0
i1 = i - o2
if i1 < 0:
i1 = 0
i2 = i
i += 1
while i < opp2:
basis[i] = 0.0
i += 1
break
else:
basis[i] = 0.0
basis[i] = 0.0 #type: ignore
# this is order 2, 3, ...
for j in range(2, order + 1):
if i2 + j >= orderpluspnts:
i2 = opp2 - j
for i in range(i1, i2 + 1):
if basis[i] != 0.0:
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
else:
d = 0.0
if basis[i + 1] != 0.0:
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
else:
e = 0.0
basis[i] = d + e
start = 1000
end = 0
for i in range(i1, i2 + 1):
if basis[i] > 0.0:
end = i
if start == 1000:
start = i
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""""BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
sum_array = [0] * nu.point_count_u
basisu = [0.0] * macro_knotsu(nu)
knots = make_knots(nu)
resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1]
uend = knots[nu.point_count_u + nu.order_u - 1] if nu.use_cyclic_u else \
knots[nu.point_count_u]
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart
while resolu:
resolu -= 1
istart, iend = basis_nurb(u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend)
#/* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] #type: ignore
sumdiv += sum_array[sum_index]
sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0
for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv #type: ignore
sum_index += 1
coord_array[coord_index: coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
if sum_array[sum_index] != 0.0:
for j in range(3):
coord_array[coord_index + j] += sum_array[sum_index] * nu.points[pt_index].co[j]
sum_index += 1
coord_index += stride
u += ustep
return coord_array
def link_object_to_collection_nested(obj: Object, col: BCollection):
if obj.name not in col.objects: #type: ignore
col.objects.link(obj)
for child in obj.children:
link_object_to_collection_nested(child, col)
def add_to_hierarchy(converted: Union[Object, BCollection], traversalContext : 'TraversalContext', converted_objects: Dict[str, Union[Object, BCollection]], preserve_transform: bool) -> None:
nextParent = traversalContext.parent
# Traverse up the tree to find a direct parent object, and a containing collection
parent_collection: Optional[BCollection] = None
parent_object: Optional[Object] = None
while nextParent:
if nextParent.current.id in converted_objects:
c = converted_objects[nextParent.current.id]
if isinstance(c, BCollection):
parent_collection = c
break
else: #isinstance(c, Object):
parent_object = parent_object or c
nextParent = nextParent.parent
# If no containing collection is found, fall back to the scene collection
if not parent_collection:
parent_collection = bpy.context.scene.collection
if isinstance(converted, Object):
if parent_object:
set_parent(converted, parent_object, preserve_transform)
link_object_to_collection_nested(converted, parent_collection)
elif converted.name not in parent_collection.children.keys():
parent_collection.children.link(converted)
def set_parent(child: Object, parent: Object, preserve_transform: bool = False) -> None:
if preserve_transform :
previous = child.matrix_world.copy() # type: ignore
child.parent = parent
child.matrix_world = previous
else:
child.parent = parent
-3
View File
@@ -1,3 +0,0 @@
from ..converter.to_native import * # noqa: F403
from ..converter.to_speckle import * # noqa: F403
from ..converter.utils import * # noqa: F403
File diff suppressed because it is too large Load Diff
@@ -1,6 +0,0 @@
from .to_speckle import convert_to_speckle # noqa: F401
from .material_to_speckle import ( # noqa: F401
blender_material_to_speckle,
create_render_material_proxies,
add_render_material_proxies_to_base,
)
@@ -1,272 +0,0 @@
from bpy.types import Object
from typing import Union, Optional, Tuple, List
from specklepy.objects.geometry import Polyline, Curve
from specklepy.objects.primitive import Interval
from specklepy.objects.base import Base
from mathutils import Matrix
from mathutils.geometry import interpolate_bezier
from .utils import nurb_make_curve, make_knots
def curve_to_speckle(
blender_obj: Object, scale_factor: float = 1.0
) -> Union[Base, None]:
assert blender_obj.type == "CURVE", "Object must be a curve"
assert blender_obj.data is not None, "Curve data cannot be None"
curve_data = blender_obj.data
matrix = blender_obj.matrix_world
units = "m" # TODO: Use the unit system from the scene
base = Base()
curves = []
for spline in curve_data.splines:
if spline.type == "BEZIER":
curves.append(
bezier_to_speckle(matrix, spline, blender_obj.name, scale_factor, units)
)
elif spline.type == "NURBS":
curves.append(
nurbs_to_speckle(matrix, spline, blender_obj.name, scale_factor, units)
)
if curves:
base["@elements"] = curves
base["name"] = blender_obj.name
return base
return None
def bezier_to_speckle(
matrix: Matrix,
spline,
name: Optional[str] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points: List[Tuple[float, float, float]] = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
transformed_point = matrix @ bp.handle_left * scale_factor
points.append(
(transformed_point.x, transformed_point.y, transformed_point.z)
)
transformed_point = matrix @ bp.co * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
if i < len(spline.bezier_points) - 1:
transformed_point = matrix @ bp.handle_right * scale_factor
points.append(
(transformed_point.x, transformed_point.y, transformed_point.z)
)
if closed:
transformed_point = (
matrix @ spline.bezier_points[-1].handle_right * scale_factor
)
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
transformed_point = matrix @ spline.bezier_points[0].handle_left * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
transformed_point = matrix @ spline.bezier_points[0].co * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
num_points = len(points)
flattened_points = []
for point in points:
flattened_points.extend(point)
knot_count = num_points + degree - 1
knots = [0] * knot_count
for i in range(1, len(knots)):
knots[i] = i // 3
length = spline.calc_length()
domain = Interval(start=0, end=length)
display_value = bezier_to_speckle_polyline(
matrix, spline, length, scale_factor, units
)
curve = Curve(
degree=degree,
periodic=not spline.use_endpoint_u,
rational=True,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
closed=spline.use_cyclic_u,
displayValue=display_value,
units=units,
bbox=None,
)
curve.__dict__["_length"] = length
curve.__dict__["_area"] = 0.0
curve["domain"] = domain
if name:
curve["name"] = name
return curve
def bezier_to_speckle_polyline(
matrix: Matrix,
spline,
length: Optional[float] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Optional[Polyline]:
segments = len(spline.bezier_points)
if segments < 2:
return None
resolution = spline.resolution_u + 1
points: List[float] = []
if not spline.use_cyclic_u:
segments -= 1
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
sampled_points = interpolate_bezier(knot1, handle1, handle2, knot2, resolution)
for p in sampled_points:
scaled_point = matrix @ p * scale_factor
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
polyline = Polyline(value=points, units=units)
polyline["domain"] = {"start": 0, "end": length}
polyline["closed"] = spline.use_cyclic_u
return polyline
def nurbs_to_speckle(
matrix: Matrix,
spline,
name: Optional[str] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
length = spline.calc_length()
domain = Interval(start=0, end=length)
weights = [pt.weight for pt in spline.points]
first_weight = weights[0] if weights else 1.0
is_rational = any(abs(w - first_weight) > 1e-9 for w in weights)
points = []
for pt in spline.points:
transformed_point = matrix @ pt.co.xyz * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
flattened_points = []
for point in points:
flattened_points.extend(point)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
resolution_multiplier = (
4 if (spline.use_cyclic_u and spline.point_count_u <= 16) else 1
)
display_value = nurbs_to_speckle_polyline(
matrix, spline, length, scale_factor, units, resolution_multiplier
)
curve = Curve(
degree=degree,
periodic=not spline.use_endpoint_u,
rational=is_rational,
points=flattened_points,
weights=weights,
knots=knots,
closed=spline.use_cyclic_u,
displayValue=display_value,
units=units,
bbox=None,
)
curve.__dict__["_length"] = length
curve["domain"] = domain
if name:
curve["name"] = name
return curve
def nurbs_to_speckle_polyline(
matrix: Matrix,
spline,
length: Optional[float] = None,
scale_factor: float = 1.0,
units: str = "m",
resolution_multiplier: int = 1,
) -> Polyline:
from mathutils import Vector
points: List[float] = []
resolution = spline.resolution_u * resolution_multiplier
sampled_points = nurb_make_curve(spline, resolution)
for i in range(0, len(sampled_points), 3):
point_vector = Vector(
(sampled_points[i], sampled_points[i + 1], sampled_points[i + 2])
)
transformed_point = matrix @ point_vector * scale_factor
points.append(transformed_point.x)
points.append(transformed_point.y)
points.append(transformed_point.z)
length = length or spline.calc_length()
polyline = Polyline(value=points, units=units)
polyline["domain"] = {"start": 0, "end": length}
polyline["closed"] = spline.use_cyclic_u
# Set length property if needed
if hasattr(polyline, "length") or hasattr(polyline, "_length"):
polyline.__dict__["_length"] = length
# Set area property if needed
if hasattr(polyline, "area") or hasattr(polyline, "_area"):
polyline.__dict__["_area"] = 0
return polyline
@@ -1,257 +0,0 @@
from typing import Dict, List, Set
import bpy
from bpy.types import Material, Object
from specklepy.objects.base import Base
from specklepy.objects.other import RenderMaterial
from specklepy.objects.proxies import RenderMaterialProxy
from ..utils import to_argb_int
from .utils import get_submesh_id, get_unique_id
def blender_material_to_speckle(material: Material) -> RenderMaterial:
"""
convert a Blender material to a Speckle RenderMaterial
"""
diffuse = -1 # default white
opacity = 1.0
emissive = -16777216 # default black
metalness = 0.0
roughness = 1.0
# extract material properties if using nodes
if material.use_nodes and material.node_tree:
output_node = None
for node in material.node_tree.nodes:
if node.type == "OUTPUT_MATERIAL":
output_node = node
break
# find the main shader node connected to output
main_shader = None
if output_node and output_node.inputs["Surface"].is_linked:
main_shader = output_node.inputs["Surface"].links[0].from_node
# handle different shader types
# we're supporting: principled, diffuse, emmision and glass - for now
if main_shader:
if main_shader.type == "BSDF_PRINCIPLED":
diffuse, opacity, metalness, roughness, emissive = (
_extract_principled_properties(main_shader)
)
elif main_shader.type == "BSDF_DIFFUSE":
color_input = main_shader.inputs.get("Color")
if color_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
diffuse = to_argb_int(rgba)
roughness = 1.0
elif main_shader.type == "EMISSION":
color_input = main_shader.inputs.get("Color")
strength_input = main_shader.inputs.get("Strength")
if color_input and strength_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
strength = (
float(strength_input.default_value)
if not strength_input.is_linked
else 1.0
)
if strength > 0:
emission_rgba = [c * strength for c in rgba[:3]] + [rgba[3]]
emission_rgba = [min(1.0, max(0.0, c)) for c in emission_rgba]
emissive = to_argb_int(emission_rgba)
diffuse = to_argb_int(rgba)
elif main_shader.type == "BSDF_GLASS":
color_input = main_shader.inputs.get("Color")
if color_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
diffuse = to_argb_int(rgba)
roughness_input = main_shader.inputs.get("Roughness")
if roughness_input:
roughness = (
float(roughness_input.default_value)
if not roughness_input.is_linked
else 0.0
)
opacity = 0.5
else:
# fallback to legacy material properties
if hasattr(material, "diffuse_color"):
rgba = list(material.diffuse_color) + [1.0]
diffuse = to_argb_int(rgba)
if hasattr(material, "metallic"):
metalness = float(material.metallic)
if hasattr(material, "roughness"):
roughness = float(material.roughness)
render_material = RenderMaterial(
name=material.name,
diffuse=diffuse,
opacity=opacity,
emissive=emissive,
metalness=metalness,
roughness=roughness,
)
return render_material
def _extract_principled_properties(principled_node):
diffuse = -1
opacity = 1.0
metalness = 0.0
roughness = 1.0
emissive = -16777216
base_color_input = principled_node.inputs.get("Base Color")
if base_color_input:
if base_color_input.is_linked:
rgba = _get_color_from_connected_node(base_color_input.links[0].from_node)
else:
rgba = list(base_color_input.default_value)
diffuse = to_argb_int(rgba)
# Alpha/Opacity
alpha_input = principled_node.inputs.get("Alpha")
if alpha_input and not alpha_input.is_linked:
opacity = float(alpha_input.default_value)
# Metallic
metallic_input = principled_node.inputs.get("Metallic")
if metallic_input and not metallic_input.is_linked:
metalness = float(metallic_input.default_value)
# Roughness
roughness_input = principled_node.inputs.get("Roughness")
if roughness_input and not roughness_input.is_linked:
roughness = float(roughness_input.default_value)
# Emission - try different possible input names for different versions
emission_color_input = principled_node.inputs.get(
"Emission Color"
) or principled_node.inputs.get("Emission")
emission_strength_input = principled_node.inputs.get("Emission Strength")
if emission_color_input:
if emission_color_input.is_linked:
emission_rgba = _get_color_from_connected_node(
emission_color_input.links[0].from_node
)
else:
emission_rgba = list(emission_color_input.default_value)
emission_strength = 1.0
if emission_strength_input and not emission_strength_input.is_linked:
emission_strength = float(emission_strength_input.default_value)
if emission_strength > 0 and any(
c > 0.01 for c in emission_rgba[:3]
): # Check if color is not black
final_emission_rgba = [c * emission_strength for c in emission_rgba[:3]] + [
emission_rgba[3]
]
final_emission_rgba = [min(1.0, max(0.0, c)) for c in final_emission_rgba]
emissive = to_argb_int(final_emission_rgba)
return diffuse, opacity, metalness, roughness, emissive
def _get_color_from_connected_node(node):
if node.type == "RGB":
rgba = list(node.outputs["Color"].default_value)
return rgba
elif node.type == "VALTORGB":
if node.color_ramp.elements:
rgba = list(node.color_ramp.elements[0].color)
return rgba
elif hasattr(node, "color"):
rgba = list(node.color) + [1.0]
return rgba
# fallback to white
return [1.0, 1.0, 1.0, 1.0]
def collect_material_assignments(objects: List[Object]) -> Dict[str, Set[str]]:
"""
collect material assignments for objects, creating unique applicationIds
for each material slot use a unique id
"""
material_assignments: Dict[str, Set[str]] = {}
for obj in objects:
if not obj or not hasattr(obj, "data") or not obj.data:
continue
# check if object has materials
if hasattr(obj.data, "materials") and obj.data.materials:
for material_index, material_slot in enumerate(obj.data.materials):
if material_slot:
material_name = material_slot.name
# set unique ID for submeshes
application_id = get_submesh_id(obj, material_index)
if material_name not in material_assignments:
material_assignments[material_name] = set()
material_assignments[material_name].add(application_id)
return material_assignments
def create_render_material_proxies(objects: List[Object]) -> List[RenderMaterialProxy]:
material_assignments = collect_material_assignments(objects)
if not material_assignments:
return []
proxies = []
for material_name, object_ids in material_assignments.items():
blender_material = bpy.data.materials.get(material_name)
if not blender_material:
continue
speckle_material = blender_material_to_speckle(blender_material)
proxy = RenderMaterialProxy(objects=list(object_ids), value=speckle_material)
proxy.applicationId = get_unique_id(blender_material)
proxies.append(proxy)
return proxies
def add_render_material_proxies_to_base(base: Base, objects: List[Object]) -> None:
"""
add render material proxies to the base object.
"""
proxies = create_render_material_proxies(objects)
if proxies:
base.renderMaterialProxies = proxies
@@ -1,132 +0,0 @@
from typing import Dict, List, cast
import bpy
from bpy.types import Mesh as BMesh
from bpy.types import MeshPolygon, Object
from mathutils import Matrix as MMatrix
from mathutils import Vector as MVector
from specklepy.objects.base import Base
from specklepy.objects.geometry.mesh import Mesh
from .utils import get_submesh_id
def mesh_to_speckle(
blender_object: Object, data: bpy.types.Mesh, units_scale: float, units: str
) -> Base:
"""
convert a Blender mesh object
"""
meshes = mesh_to_speckle_meshes(blender_object, data, units_scale, units)
return meshes
def mesh_to_speckle_meshes(
blender_object: Object, data: bpy.types.Mesh, units_scale: float, units: str
) -> List[Mesh]:
"""
convert a Blender mesh to a list of Speckle meshes
each face corner (loop) gets its own vertex
"""
assert isinstance(data, BMesh), "Data must be a Blender mesh"
assert units_scale > 0, "Units scale must be positive"
submesh_data: Dict[int, List[MeshPolygon]] = {}
for p in data.polygons:
if p.material_index not in submesh_data:
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world)
normal_transform = transform.to_3x3().inverted().transposed()
submeshes = []
# sort material indices to ensure consistent ordering
for material_index in sorted(submesh_data.keys()):
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
m_normals: List[float] = []
vertex_counter = 0
for face in submesh_data[material_index]:
mesh_area += face.area
loop_indices = face.loop_indices
m_faces.append(len(loop_indices))
for loop_index in loop_indices:
loop = data.loops[loop_index]
vertex = data.vertices[loop.vertex_index]
transformed_vertex = transform @ vertex.co * units_scale
m_verts.extend(
[transformed_vertex.x, transformed_vertex.y, transformed_vertex.z]
)
# get and transform the loop normal
# try to get split normal, fallback to face normal if not available
try:
if hasattr(loop, "normal") and len(loop.normal) > 0:
# Use split normal from loop
loop_normal = normal_transform @ loop.normal
else:
# Fallback to face normal
loop_normal = normal_transform @ face.normal
except: # noqa: E722
# Final fallback: use face normal
loop_normal = normal_transform @ face.normal
loop_normal.normalize()
m_normals.extend([loop_normal.x, loop_normal.y, loop_normal.z])
# add UV coordinates if available
if data.uv_layers.active:
uv_data = data.uv_layers.active.data[loop_index]
uv = cast(MVector, uv_data.uv)
m_texcoords.extend([uv.x, uv.y])
m_faces.append(vertex_counter)
vertex_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
vertexNormals=m_normals,
units=units,
)
if len(m_verts) > 0:
speckle_mesh.area = mesh_area
speckle_mesh.applicationId = get_submesh_id(blender_object, material_index)
submeshes.append(speckle_mesh)
return submeshes
def is_closed_mesh(faces: List[int]) -> bool:
"""
check if a mesh is closed by verifying that each edge is shared by exactly 2 faces.
"""
edge_counts = {}
i = 0
while i < len(faces):
vertex_count = faces[i]
for j in range(vertex_count):
v1 = faces[i + 1 + j]
v2 = faces[i + 1 + ((j + 1) % vertex_count)]
edge = tuple(sorted([v1, v2]))
edge_counts[edge] = edge_counts.get(edge, 0) + 1
i += vertex_count + 1
return all(count == 2 for count in edge_counts.values())
@@ -1,17 +0,0 @@
from bpy.types import Object
from specklepy.objects.geometry import Point
def point_to_speckle(blender_object: Object, scale_factor: float = 1.0) -> Point:
assert blender_object.type == "EMPTY", "Object must be an empty."
location = blender_object.location
speckle_point = Point(
x=location.x * scale_factor,
y=location.y * scale_factor,
z=location.z * scale_factor,
units="", # TODO: implement units in object level
)
return speckle_point
@@ -1,85 +0,0 @@
from bpy.types import Object
from typing import Optional
from specklepy.objects.data_objects import BlenderObject
from .curve_to_speckle import curve_to_speckle
from .mesh_to_speckle import mesh_to_speckle_meshes
from .utils import get_object_id, get_curve_element_id
def convert_to_speckle(
blender_object: Object,
scale_factor: float = 1.0,
units: str = "m",
apply_modifiers: bool = True,
) -> Optional[BlenderObject]:
display_value = []
properties = {}
if blender_object.type == "CURVE":
# handle curve modifiers apply_modifiers is True
if apply_modifiers and blender_object.modifiers:
import bpy
# Convert curve with modifiers to mesh
depsgraph = bpy.context.evaluated_depsgraph_get()
evaluated_obj = blender_object.evaluated_get(depsgraph)
evaluated_mesh = evaluated_obj.to_mesh()
if evaluated_mesh:
meshes = mesh_to_speckle_meshes(
blender_object, evaluated_mesh, scale_factor, units
)
blender_object.to_mesh_clear()
if meshes:
display_value = meshes
else:
# curve conversion without modifiers
curve_result = curve_to_speckle(blender_object, scale_factor)
if curve_result and hasattr(curve_result, "@elements"):
display_value = curve_result["@elements"]
for i, element in enumerate(display_value):
if hasattr(element, "applicationId"):
element.applicationId = get_curve_element_id(blender_object, i)
elif curve_result:
if hasattr(curve_result, "applicationId"):
curve_result.applicationId = get_curve_element_id(blender_object, 0)
display_value = [curve_result]
elif blender_object.type == "MESH":
# get mesh data - apply modifiers if requested
mesh_data = blender_object.data
if apply_modifiers and blender_object.modifiers:
import bpy
# use evaluated object to get mesh with modifiers applied
depsgraph = bpy.context.evaluated_depsgraph_get()
evaluated_obj = blender_object.evaluated_get(depsgraph)
evaluated_mesh = evaluated_obj.to_mesh()
mesh_data = evaluated_mesh
meshes = mesh_to_speckle_meshes(blender_object, mesh_data, scale_factor, units)
if (
apply_modifiers
and blender_object.modifiers
and mesh_data != blender_object.data
):
blender_object.to_mesh_clear()
if meshes:
display_value = meshes
if not display_value:
return None
if not isinstance(display_value, list):
display_value = [display_value]
return BlenderObject(
name=blender_object.name,
type=blender_object.type,
displayValue=display_value,
applicationId=get_object_id(blender_object),
properties=properties,
units=units,
)
-242
View File
@@ -1,242 +0,0 @@
import bpy
from bpy.types import ID, Object
import math
from typing import Tuple, Optional
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
SPECKLE_ID_LENGTH = 32
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = (
len(blender_object.name) > _QUICK_TEST_NAME_LENGTH
and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
)
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * macro_knotsu(nu)
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order
if flag == 1: # CU_NURB_ENDPOINT
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2: # CU_NURB_BEZIER
if order == 4:
k = 0.34
for a in range(pts_order):
knots[a] = math.floor(k)
k += 1.0 / 3.0
elif order == 3:
k = 0.6
for a in range(pts_order):
if a >= order and a <= point_count:
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(1, len(knots) - 1):
knots[a] = a - 1
knots[-1] = knots[-2]
def basis_nurb(
t: float,
order: int,
point_count: int,
knots: list[float],
basis: list[float],
start: int,
end: int,
) -> Tuple[int, int]:
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
# this part is order '1'
o2 = order + 1
for i in range(opp2):
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
basis[i] = 1.0
i1 = i - o2
if i1 < 0:
i1 = 0
i2 = i
i += 1
while i < opp2:
basis[i] = 0.0
i += 1
break
else:
basis[i] = 0.0
basis[i] = 0.0 # type: ignore
# this is order 2, 3, ...
for j in range(2, order + 1):
if i2 + j >= orderpluspnts:
i2 = opp2 - j
for i in range(i1, i2 + 1):
if basis[i] != 0.0:
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
else:
d = 0.0
if basis[i + 1] != 0.0:
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
else:
e = 0.0
basis[i] = d + e
start = 1000
end = 0
for i in range(i1, i2 + 1):
if basis[i] > 0.0:
end = i
if start == 1000:
start = i
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""" "BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
sum_array = [0] * nu.point_count_u
basisu = [0.0] * macro_knotsu(nu)
knots = make_knots(nu)
resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1]
uend = (
knots[nu.point_count_u + nu.order_u - 1]
if nu.use_cyclic_u
else knots[nu.point_count_u]
)
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart
while resolu:
resolu -= 1
istart, iend = basis_nurb(
u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend
)
# /* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] # type: ignore
sumdiv += sum_array[sum_index]
sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0
for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv # type: ignore
sum_index += 1
coord_array[coord_index : coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
if sum_array[sum_index] != 0.0:
for j in range(3):
coord_array[coord_index + j] += (
sum_array[sum_index] * nu.points[pt_index].co[j]
)
sum_index += 1
coord_index += stride
u += ustep
return coord_array
def get_unique_id(native_object: ID, suffix: Optional[str] = None) -> str:
base_id = f"{type(native_object).__name__}:{native_object.name_full}"
if suffix:
return f"{base_id}:{suffix}"
return base_id
def get_submesh_id(blender_object: Object, material_index: int) -> str:
mesh_data = blender_object.data
if not mesh_data:
return f"Mesh:{blender_object.name_full}_mat{material_index}"
return f"Mesh:{mesh_data.name_full}_mat{material_index}"
def get_curve_element_id(blender_object: Object, curve_index: int = 0) -> str:
curve_data = blender_object.data
if not curve_data:
return f"Curve:{blender_object.name_full}_curve{curve_index}"
if curve_index == 0:
return f"Curve:{curve_data.name_full}"
return f"Curve:{curve_data.name_full}_curve{curve_index}"
def get_object_id(blender_object: Object) -> str:
return get_unique_id(blender_object)
-188
View File
@@ -1,188 +0,0 @@
from typing import Tuple, List, Optional
import bpy
import mathutils
from specklepy.objects import Base
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
"""
converts the int representation of a colour into a RGBA tuple
"""
alpha = ((argb_int >> 24) & 255) / 255
red = ((argb_int >> 16) & 255) / 255
green = ((argb_int >> 8) & 255) / 255
blue = (argb_int & 255) / 255
return (red, green, blue, alpha)
def to_argb_int(rgba_color: List[float]) -> int:
"""
converts an RGBA array to an ARGB integer
"""
argb_color = rgba_color[-1:] + rgba_color[:3]
int_color = [int(val * 255) for val in argb_color]
return int.from_bytes(int_color, byteorder="big", signed=True)
def create_material_from_proxy(
render_material, material_name: str
) -> bpy.types.Material:
"""
creates a Blender material from a Speckle RenderMaterial
"""
if material_name in bpy.data.materials:
return bpy.data.materials[material_name]
# create new material
material = bpy.data.materials.new(name=material_name)
material.use_nodes = True
node_tree = material.node_tree
nodes = node_tree.nodes
for node in nodes:
nodes.remove(node)
bsdf = nodes.new(type="ShaderNodeBsdfPrincipled")
output = nodes.new(type="ShaderNodeOutputMaterial")
node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
if hasattr(render_material, "diffuse"):
diffuse_rgba = to_rgba(render_material.diffuse)
bsdf.inputs["Base Color"].default_value = (
diffuse_rgba[0],
diffuse_rgba[1],
diffuse_rgba[2],
1.0,
)
if hasattr(render_material, "opacity"):
opacity = float(render_material.opacity)
if opacity < 1.0:
material.blend_method = "BLEND"
bsdf.inputs["Alpha"].default_value = opacity
if hasattr(render_material, "metalness"):
metalness = float(render_material.metalness)
bsdf.inputs["Metallic"].default_value = metalness
if hasattr(render_material, "roughness"):
roughness = float(render_material.roughness)
bsdf.inputs["Roughness"].default_value = roughness
if (
hasattr(render_material, "emissive") and render_material.emissive != -16777216
): # default black
emissive_rgba = to_rgba(render_material.emissive)
# only add emission if it's not black (default)
if any(val > 0.01 for val in emissive_rgba[:3]):
bsdf.inputs["Emission Color"].default_value = (
emissive_rgba[0],
emissive_rgba[1],
emissive_rgba[2],
1.0,
)
bsdf.inputs["Emission Strength"].default_value = 1.0
# set viewport display color
if hasattr(render_material, "diffuse") and hasattr(render_material, "opacity"):
material.diffuse_color = (
diffuse_rgba[0],
diffuse_rgba[1],
diffuse_rgba[2],
opacity,
)
return material
def transform_matrix(transform: List[float]) -> mathutils.Matrix:
"""
converts a speckle transform array to a 4x4 matrix (blender needs it)
"""
if len(transform) != 16:
raise ValueError(f"Expected transform with 16 values, got {len(transform)}")
return mathutils.Matrix(
(
(transform[0], transform[4], transform[8], transform[12]),
(transform[1], transform[5], transform[9], transform[13]),
(transform[2], transform[6], transform[10], transform[14]),
(transform[3], transform[7], transform[11], transform[15]),
)
)
def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
"""
finds an object using traversal, checking both id and applicationId
"""
if hasattr(root_object, "__closure") and root_object.__closure:
if target_id in root_object.__closure:
if hasattr(root_object, "elements"):
for element in root_object.elements:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
return find_object_by_id(root_object, element.referencedId)
if hasattr(root_object, "@elements"):
for element in root_object["@elements"]:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
return find_object_by_id(root_object, element.referencedId)
traversal_function = create_default_traversal_function()
for traversal_item in traversal_function.traverse(root_object):
obj = traversal_item.current
if not hasattr(obj, "id"):
continue
if obj.id == target_id:
return obj
if hasattr(obj, "applicationId"):
app_id = obj.applicationId
if app_id == target_id:
return obj
def deep_search(search_obj):
if hasattr(search_obj, "id") and search_obj.id == target_id:
return search_obj
elements_attrs = ["elements", "@elements"]
for attr in elements_attrs:
if hasattr(search_obj, attr):
elements = getattr(search_obj, attr)
if elements and isinstance(elements, list):
for element in elements:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
ref_obj = find_object_by_id(
root_object, element.referencedId
)
if ref_obj:
return ref_obj
result = deep_search(element)
if result:
return result
return None
return deep_search(root_object)
+45
View File
@@ -0,0 +1,45 @@
from typing import Callable
from specklepy.objects.base import Base
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string
def _report(msg: object) -> None:
"""
Function for printing messages to the console
"""
print("SpeckleBlender: {}".format(msg))
def get_scale_length(units: str) -> float:
"""Returns a scalar to convert distance values from one unit system to meters"""
return get_scale_factor_to_meters(get_units_from_string(units))
def get_default_traversal_func(can_convert_to_native: Callable[[Base], bool]) -> GraphTraversal:
"""
Traversal func for traversing a speckle commit object
"""
ignore_rule = TraversalRule(
[
lambda o: "Objects.Structural.Results" in o.speckle_type, #Sadly, this one is necessary to avoid double conversion...
lambda o: "Objects.BuiltElements.Revit.Parameter" in o.speckle_type, #This one is just for traversal performance of revit commits
],
lambda _: [],
)
convertible_rule = TraversalRule(
[can_convert_to_native],
lambda _: ELEMENTS_PROPERTY_ALIASES,
)
default_rule = TraversalRule(
[lambda _: True],
lambda o: o.get_member_names(), #TODO: avoid deprecated members
)
return GraphTraversal([ignore_rule, convertible_rule, default_rule])
+22 -42
View File
@@ -1,7 +1,6 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import Path
@@ -56,7 +55,9 @@ def user_application_data_path() -> Path:
if sys.platform.startswith("win"):
app_data_path = os.getenv("APPDATA")
if not app_data_path:
raise Exception("Cannot get appdata path from environment.")
raise Exception(
"Cannot get appdata path from environment."
)
return Path(app_data_path)
else:
# try getting the standard XDG_DATA_HOME value
@@ -67,7 +68,9 @@ def user_application_data_path() -> Path:
else:
return _ensure_folder_exists(Path.home(), ".config")
except Exception as ex:
raise Exception("Failed to initialize user application data path.", ex)
raise Exception(
"Failed to initialize user application data path.", ex
)
def user_speckle_folder_path() -> Path:
@@ -87,16 +90,19 @@ def user_speckle_connector_installation_path(host_application: str) -> Path:
)
print("Starting module dependency installation")
print(sys.executable)
PYTHON_PATH = sys.executable
def connector_installation_path(host_application: str) -> Path:
connector_installation_path = user_speckle_connector_installation_path(
host_application
)
connector_installation_path = user_speckle_connector_installation_path(host_application)
connector_installation_path.mkdir(exist_ok=True, parents=True)
# set user modules path at beginning of paths for earlier hit
@@ -107,6 +113,7 @@ def connector_installation_path(host_application: str) -> Path:
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
@@ -125,14 +132,13 @@ def ensure_pip() -> None:
if completed_process.returncode == 0:
print("Successfully installed pip")
else:
raise Exception(
f"Failed to install pip, got {completed_process.returncode} return code"
)
raise Exception(f"Failed to install pip, got {completed_process.returncode} return code")
def get_requirements_path() -> Path:
# we assume that a requirements.txt exists next to the __init__.py file
path = Path(Path(__file__).parent, "requirements.txt")
assert path.exists()
return path
@@ -141,55 +147,30 @@ def install_requirements(host_application: str) -> None:
# script path. Here we'll install the
# dependencies
path = connector_installation_path(host_application)
print(f"Installing Speckle dependencies to {path}")
from subprocess import run
def debugger_is_active() -> bool:
"""Return if the debugger is currently active"""
return hasattr(sys, "gettrace") and sys.gettrace() is not None
requirements_path = get_requirements_path()
is_debug = debugger_is_active()
if not is_debug and not requirements_path.exists():
print("Skipped installing dependencies")
return
print(f"Installing Speckle dependencies to {path}")
completed_process = run(
[
PYTHON_PATH,
"-m",
"pip",
"-q",
"--disable-pip-version-check",
"install",
"--prefer-binary",
"--ignore-installed",
"--no-compile",
"--no-deps",
"-t",
str(path),
"-r",
str(requirements_path),
str(get_requirements_path()),
],
capture_output=True,
text=True,
)
if completed_process.returncode != 0:
print(completed_process.stdout)
print(completed_process.stderr)
m = f"Failed to install dependencies through pip, got {completed_process.returncode} return code"
m = f"Failed to install dependenices through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
print("Successfully installed dependencies")
if not is_debug:
requirements_path.unlink()
def install_dependencies(host_application: str) -> None:
if not is_pip_available():
@@ -203,7 +184,7 @@ def _import_dependencies() -> None:
# the code above doesn't work for now, it fails on importing graphql-core
# despite that, the connector seams to be working as expected
# But it would be nice to make this solution work
# it would ensure that all dependencies are fully loaded
# it would ensure that all dependencies are fully loaded
# requirements = get_requirements_path().read_text()
# reqs = [
# req.split(" ; ")[0].split("==")[0].split("[")[0].replace("-", "_")
@@ -214,7 +195,6 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
@@ -222,6 +202,6 @@ def ensure_dependencies(host_application: str) -> None:
_import_dependencies()
print("Successfully found dependencies")
except ImportError:
raise Exception(
f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!"
)
raise Exception(f"Cannot automatically ensure Speckle dependencies. Please try restarting the host application {host_application}!")
+62
View File
@@ -0,0 +1,62 @@
from .users import LoadUsers, LoadUserStreams, ResetUsers
from .object import (
UpdateObject,
ResetObject,
DeleteObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
)
from .streams import (
ReceiveStreamObjects,
SendStreamObjects,
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
)
from .streams import (
AddStreamFromURL,
CreateStream,
CopyStreamId,
CopyCommitId,
CopyBranchName,
)
from .commit import DeleteCommit
from .misc import OpenSpeckleGuide, OpenSpeckleTutorials, OpenSpeckleForum
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
CopyStreamId,
CopyCommitId,
CopyBranchName,
]
operator_classes.extend([DeleteCommit])
operator_classes.extend(
[
UpdateObject,
ResetObject,
DeleteObject,
UploadNgonsAsPolylines,
SelectIfSameCustomProperty,
SelectIfHasCustomProperty,
]
)
operator_classes.extend(
[
ViewStreamDataApi,
DeleteStream,
SelectOrphanObjects,
AddStreamFromURL,
CreateStream,
OpenSpeckleGuide,
OpenSpeckleTutorials,
OpenSpeckleForum,
]
)
+72
View File
@@ -0,0 +1,72 @@
"""
Commit operators
"""
import bpy
from bpy.props import BoolProperty
from bpy_speckle.clients import speckle_clients
from bpy_speckle.functions import _report
from bpy_speckle.properties.scene import get_speckle
from specklepy.logging import metrics
class DeleteCommit(bpy.types.Operator):
"""
Deletes the selected commit from the selected stream.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
"""
bl_idname = "speckle.delete_commit"
bl_label = "Delete commit"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Delete active commit permanently"
are_you_sure: BoolProperty(
name="Confirm",
default=False,
)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "are_you_sure")
def invoke(self, context, event):
speckle = get_speckle(context)
wm = context.window_manager
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
if not self.are_you_sure:
_report("Cancelled by user")
return {"CANCELLED"}
self.are_you_sure = False
self.delete_commit(context)
return {"FINISHED"}
@staticmethod
def delete_commit(context: bpy.types.Context) -> None:
speckle = get_speckle(context)
(_, stream, _, commit) = speckle.validate_commit_selection()
client = speckle_clients[int(speckle.active_user)]
deleted = client.commit.delete(stream_id=stream.id, commit_id=commit.id)
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_commit"
},
)
if not deleted:
raise Exception("Delete operation failed")
print(f"Commit {commit.id} ({commit.message}) has been deleted from stream {stream.id}")
+59
View File
@@ -0,0 +1,59 @@
import bpy
import webbrowser
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator):
bl_idname = "speckle.open_speckle_guide"
bl_label = "Speckle Guide"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Browse the documentation on the Speckle Guide"
def execute(self, context):
webbrowser.open("https://speckle.guide/user/blender.html")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleGuide"
},
)
return {"FINISHED"}
class OpenSpeckleTutorials(bpy.types.Operator):
bl_idname = "speckle.open_speckle_tutorials"
bl_label = "Tutorials Portal"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Visit our tutorials portal for learning resources"
def execute(self, context):
webbrowser.open("https://speckle.systems/tutorials/")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleTutorials"
},
)
return {"FINISHED"}
class OpenSpeckleForum(bpy.types.Operator):
bl_idname = "speckle.open_speckle_forum"
bl_label = "Community Forum"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Ask questions and join the discussion on our community forum"
def execute(self, context):
webbrowser.open("https://speckle.community/")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "OpenSpeckleForum"
},
)
return {"FINISHED"}
+366
View File
@@ -0,0 +1,366 @@
"""
Object operators
"""
import bpy
from bpy.props import BoolProperty, EnumProperty
from deprecated import deprecated
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
ngons_to_speckle_polylines,
)
from bpy_speckle.functions import get_scale_length, _report
from bpy_speckle.clients import speckle_clients
from specklepy.logging import metrics
@deprecated
class UpdateObject(bpy.types.Operator):
"""
Update local (receive) or remote (send) object depending on
the update direction. If sending, updates the object on the
server in-place.
"""
bl_idname = "speckle.update_object"
bl_label = "Update Object"
bl_options = {"REGISTER", "UNDO"}
client = None
def execute(self, context):
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
active = context.active_object
_report(active)
if active is not None and active.speckle.enabled:
if active.speckle.send_or_receive == "send" and active.speckle.stream_id:
sstream = client.streams.get(active.speckle.stream_id)
# res = client.StreamGetAsync(active.speckle.stream_id)['resource']
# res = client.streams.get(active.speckle.stream_id)
if sstream is None:
_report("Getting stream failed.")
return {"CANCELLED"}
stream_units = "Meters"
if sstream.baseProperties:
stream_units = sstream.baseProperties.units
scale = context.scene.unit_settings.scale_length / get_scale_length(
stream_units
)
sm = convert_to_speckle(active, scale)
_report("Updating object {}".format(sm["_id"]))
client.objects.update(active.speckle.object_id, sm)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UpdateObject"
},
)
return {"FINISHED"}
return {"CANCELLED"}
return {"CANCELLED"}
@deprecated
class ResetObject(bpy.types.Operator):
"""
Reset Speckle object settings
"""
bl_idname = "speckle.reset_object"
bl_label = "Reset Object"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
context.object.speckle.send_or_receive = "send"
context.object.speckle.stream_id = ""
context.object.speckle.object_id = ""
context.object.speckle.enabled = False
context.view_layer.update()
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetObject"
},
)
return {"FINISHED"}
@deprecated
class DeleteObject(bpy.types.Operator):
"""
Delete object from the server and update relevant stream
"""
bl_idname = "speckle.delete_object"
bl_label = "Delete Object"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
client = speckle_clients[int(context.scene.speckle.active_user)]
active = context.object
if active.speckle.enabled:
res = client.StreamGetAsync(active.speckle.stream_id)
existing = [
x
for x in res["resource"]["objects"]
if x["_id"] == active.speckle.object_id
]
if existing is None:
return {"CANCELLED"}
new_objects = [
x
for x in res["resource"]["objects"]
if x["_id"] != active.speckle.object_id
]
res = client.GetLayers(active.speckle.stream_id)
new_layers = res["resource"]["layers"]
new_layers[-1]["objectCount"] = new_layers[-1]["objectCount"] - 1
new_layers[-1]["topology"] = "0-%s" % new_layers[-1]["objectCount"]
res = client.StreamUpdateAsync(
{"objects": new_objects, "layers": new_layers}, active.speckle.stream_id
)
res = client.ObjectDeleteAsync(active.speckle.object_id)
active.speckle.send_or_receive = "send"
active.speckle.stream_id = ""
active.speckle.object_id = ""
active.speckle.enabled = False
context.view_layer.update()
metrics.track(
"Connector Action",
None,
custom_props={
"name": "DeleteObject"
},
)
return {"FINISHED"}
@deprecated
class UploadNgonsAsPolylines(bpy.types.Operator):
"""
Upload mesh ngon faces as polyline outlines
TODO: move to another category of specialized operators and fix to work with API 2.0
"""
bl_idname = "speckle.upload_ngons_as_polylines"
bl_label = "Upload Ngons As Polylines"
bl_options = {"REGISTER", "UNDO"}
clear_stream: BoolProperty(
name="Clear stream",
default=False,
)
def execute(self, context):
active = context.active_object
if active is not None and active.type == "MESH":
user = context.scene.speckle.users[int(context.scene.speckle.active_user)]
client = speckle_clients[int(context.scene.speckle.active_user)]
stream = user.streams[user.active_stream]
# scale = context.scene.unit_settings.scale_length / get_scale_length(
# stream.units
# )
scale = 1.0
sp = ngons_to_speckle_polylines(active, scale)
if sp is None:
return {"CANCELLED"}
placeholders = []
for polyline in sp:
res = client.objects.create([polyline])
if res is None:
_report(client.me)
continue
placeholders.extend(res)
if not placeholders:
return {"CANCELLED"}
# Get list of existing objects in stream and append new object to list
_report("Fetching stream...")
sstream = client.streams.get(stream.id)
if self.clear_stream:
_report("Clearing stream...")
sstream.objects = placeholders
N = 0
else:
sstream.objects.extend(placeholders)
N = sstream.layers[-1].objectCount
if self.clear_stream:
N = 0
sstream.layers[-1].objectCount = N + len(placeholders)
sstream.layers[-1].topology = "0-%s" % (N + len(placeholders))
res = client.streams.update(sstream.id, sstream)
# Update view layer
context.view_layer.update()
_report("Done.")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "UploadNgonsAsPolylines"
},
)
return {"FINISHED"}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "clear_stream")
def get_custom_speckle_props(self, context):
ignore = ["speckle", "cycles", "cycles_visibility"]
active = context.active_object
if not active:
return []
return [(x, "{}".format(x), "") for x in active.keys()]
@deprecated
class SelectIfSameCustomProperty(bpy.types.Operator):
"""
Select scene objects if they have the same custom property
value as the active object
"""
bl_idname = "speckle.select_if_same_custom_props"
bl_label = "Select Identical Custom Props"
bl_options = {"REGISTER", "UNDO"}
custom_prop: EnumProperty(
name="Custom properties",
description="Available streams associated with user.",
items=get_custom_speckle_props,
)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "custom_prop")
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
active = context.active_object
if not active:
return {"CANCELLED"}
if self.custom_prop not in active.keys():
return {"CANCELLED"}
value = active[self.custom_prop]
_report(
"Looking for '{}' property with a value of '{}'.".format(
self.custom_prop, value
)
)
for obj in bpy.data.objects:
if self.custom_prop in obj.keys() and obj[self.custom_prop] == value:
obj.select_set(True)
else:
obj.select_set(False)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfSameCustomProperty"
},
)
return {"FINISHED"}
@deprecated
class SelectIfHasCustomProperty(bpy.types.Operator):
"""
Select scene objects if they have the same custom property
as the active object, regardless of the value
"""
bl_idname = "speckle.select_if_has_custom_props"
bl_label = "Select Same Custom Prop"
bl_options = {"REGISTER", "UNDO"}
custom_prop: EnumProperty(
name="Custom properties",
description="Custom properties yo",
items=get_custom_speckle_props,
)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "custom_prop")
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def execute(self, context):
active = context.active_object
if not active:
return {"CANCELLED"}
if self.custom_prop not in active.keys():
return {"CANCELLED"}
value = active[self.custom_prop]
_report("Looking for '{}' property.".format(self.custom_prop))
for obj in bpy.data.objects:
if self.custom_prop in obj.keys():
obj.select_set(True)
else:
obj.select_set(False)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "SelectIfHasCustomProperty"
},
)
return {"FINISHED"}
+775
View File
@@ -0,0 +1,775 @@
"""
Stream operators
"""
from math import radians
from typing import Callable, Dict, Optional, Tuple, Union, cast
import webbrowser
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
EnumProperty,
)
from bpy.types import (
Context,
Object,
Collection
)
from deprecated import deprecated
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.convert.to_native import (
can_convert_to_native,
collection_to_native,
convert_to_native,
set_convert_instances_as,
)
from bpy_speckle.convert.to_speckle import (
convert_to_speckle,
)
from bpy_speckle.functions import (
get_default_traversal_func,
_report,
get_scale_length,
)
from bpy_speckle.clients import speckle_clients
from bpy_speckle.operators.users import LoadUserStreams, add_user_stream
from bpy_speckle.properties.scene import SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle
from bpy_speckle.convert.util import ConversionSkippedException, add_to_hierarchy
from specklepy.core.api.models import Commit
from specklepy.core.api import operations, host_applications
from specklepy.core.api.wrapper import StreamWrapper
from specklepy.core.api.resources.stream import Stream
from specklepy.transports.server import ServerTransport
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
from specklepy.logging.exceptions import SpeckleException
from specklepy.logging import metrics
ObjectCallback = Optional[Callable[[bpy.types.Context, Object, Base], Object]]
ReceiveCompleteCallback = Optional[Callable[[bpy.types.Context, Dict[str, Union[Object, Collection]]], None]]
def get_receive_funcs(speckle: SpeckleSceneSettings) -> tuple[ObjectCallback, ReceiveCompleteCallback]:
"""
Fetches the injected callback functions from user specified "Receive Script"
"""
objectCallback: ObjectCallback = None
receiveCompleteCallback: ReceiveCompleteCallback = None
if speckle.receive_script in bpy.data.texts:
mod = bpy.data.texts[speckle.receive_script].as_module()
if hasattr(mod, "execute_for_each"):
objectCallback = mod.execute_for_each #type: ignore
elif hasattr(mod, "execute"):
objectCallback = lambda c, o, _ : mod.execute(c.scene, o) #type: ignore
if hasattr(mod, "execute_for_all"):
receiveCompleteCallback = mod.execute_for_all #type: ignore
return (objectCallback, receiveCompleteCallback)
#RECEIVE_MODES = [#TODO: modes
# ("create", "Create", "Add new geometry, without removing any existing objects"),
# ("replace", "Replace", "Replace objects from previous receive operations from the same stream"),
# #("update","Update", "") #TODO: update mode!
#]
INSTANCES_SETTINGS = [
("collection_instance", "Collection Instance", "Receive Instances as Collection Instances"),
("linked_duplicates", "Linked Duplicates", "Receive Instances as Linked Duplicates"),
]
class ReceiveStreamObjects(bpy.types.Operator):
"""
Receive stream objects
"""
bl_idname = "speckle.receive_stream_objects"
bl_label = "Download Stream Objects"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Receive objects from active stream"
clean_meshes: BoolProperty(name="Clean Meshes", default=False) # type: ignore
#receive_mode: EnumProperty(items=RECEIVE_MODES, name="Receive Type", default="replace", description="The behaviour of the receive operation")
receive_instances_as: EnumProperty(items=INSTANCES_SETTINGS, name="Receive Instances As", default="collection_instance", description="How to receive speckle Instances") # type: ignore
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "clean_meshes")
#col.prop(self, "receive_mode")
col.prop(self, "receive_instances_as")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
@staticmethod
def clean_converted_meshes(context: bpy.types.Context, convertedObjects: dict[str, Object]):
bpy.ops.object.select_all(action='DESELECT')
active = None
for obj in convertedObjects.values():
if obj.type != 'MESH':
continue
obj.select_set(True, view_layer=context.scene.view_layers[0])
active = obj
if active == None:
return
context.view_layer.objects.active = active
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.dissolve_limited(angle_limit=radians(0.1))
# Reset state to previous (not quite sure if this is 100% necessary)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
bpy.context.view_layer.objects.active = None # type: ignore
def execute(self, context):
self.receive(context)
return {"FINISHED"}
def receive(self, context: Context) -> None:
bpy.context.view_layer.objects.active = None # type: ignore
speckle = get_speckle(context)
(user, stream, branch, commit) = speckle.validate_commit_selection()
client = speckle_clients[int(speckle.active_user)]
transport = ServerTransport(stream.id, client)
# Fetch commit data
commit_object = operations.receive(commit.referenced_object, transport)
client.commit.received(
stream.id,
commit.id,
source_application="blender",
message="received commit from Speckle Blender",
)
metrics.track(
metrics.RECEIVE,
getattr(transport, "account", None),
custom_props={
"sourceHostApp": host_applications.get_host_app_from_string(commit.source_application).slug,
"sourceHostAppVersion": commit.source_application,
"isMultiplayer": commit.author_id != user.id,
#"connector_version": "unknown", #TODO
},
)
# Convert received data
context.window_manager.progress_begin(0, commit_object.totalChildrenCount or 1)
set_convert_instances_as(self.receive_instances_as) #HACK: we need a better way to pass settings down to the converter
traversalFunc = get_default_traversal_func(can_convert_to_native)
converted_objects: Dict[str, Union[Object, Collection]] = {}
converted_count: int = 0
(object_converted_callback, on_complete_callback) = get_receive_funcs(speckle)
# older commits will have a non-collection root object
# for the sake of consistent behaviour, we will wrap any non-collection commit objects in a collection
if not isinstance(commit_object, SCollection):
dummy_commit_object = SCollection()
dummy_commit_object.elements = [commit_object]
dummy_commit_object.name = getattr(commit_object, "name", None)
dummy_commit_object.id = dummy_commit_object.get_id()
commit_object = dummy_commit_object
# ensure commit object has a name if not already
if not commit_object.name:
commit_object.name = "{} [ {} @ {} ]".format(stream.name, branch.name, commit.id) # Matches Rhino "Create" naming
for item in traversalFunc.traverse(commit_object):
current: Base = item.current
if can_convert_to_native(current) or isinstance(current, SCollection):
try:
if not current or not current.id:
raise Exception(f"{current} was an invalid speckle object")
#Convert the object!
converted_data_type: str
converted: Union[Object, Collection, None]
if isinstance(current, SCollection):
if(current.collectionType == "Scene Collection"): raise ConversionSkippedException()
converted = collection_to_native(current)
converted_data_type = "COLLECTION"
else:
converted = convert_to_native(current)
converted_data_type = "COLLECTION_INSTANCE" if converted.instance_collection else str(converted.type)
#Run the user specified callback function (AKA receive script)
if object_converted_callback:
converted = object_converted_callback(context, converted, current)
if converted is None:
raise Exception("Conversion returned None")
converted_objects[current.id] = converted
add_to_hierarchy(converted, item, converted_objects, True)
_report(f"Successfully converted {type(current).__name__} {current.id} as '{converted_data_type}'")
except ConversionSkippedException as ex:
_report(f"Skipped converting {type(current).__name__} {current.id}: {ex}")
except Exception as ex:
_report(f"Failed to converted {type(current).__name__} {current.id}: {ex}")
converted_count += 1
context.window_manager.progress_update(converted_count) #NOTE: We don't expect to ever reach 100% since not every object will be traversed
context.window_manager.progress_end()
if self.clean_meshes:
objects = {k: v for k, v in converted_objects.items() if isinstance(v, Object)}
self.clean_converted_meshes(context, objects)
if on_complete_callback:
on_complete_callback(context, converted_objects)
class SendStreamObjects(bpy.types.Operator):
"""
Send stream objects
"""
bl_idname = "speckle.send_stream_objects"
bl_label = "Send stream objects"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Send selected objects to active stream"
apply_modifiers: BoolProperty(name="Apply modifiers", default=True) # type: ignore
commit_message: StringProperty(
name="Message",
default="Pushed elements from Blender.",
) # type: ignore
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "commit_message")
col.prop(self, "apply_modifiers")
def invoke(self, context, event):
wm = context.window_manager
speckle = get_speckle(context)
if len(speckle.users) <= 0:
_report("No user accounts")
return {"CANCELLED"}
N = len(context.selected_objects)
if N == 1:
self.commit_message = f"Pushed {N} element from Blender."
else:
self.commit_message = f"Pushed {N} elements from Blender."
return wm.invoke_props_dialog(self)
def execute(self, context):
self.send(context)
return {"FINISHED"}
def send(self, context: Context) -> None:
selected = context.selected_objects
if len(selected) < 1:
raise Exception("No objects are selected, sending canceled")
speckle = get_speckle(context)
(user, stream, branch) = speckle.validate_branch_selection()
client = speckle_clients[int(speckle.active_user)]
units = "m" if bpy.context.scene.unit_settings.system == "METRIC" else "ft"
units_scale = context.scene.unit_settings.scale_length / get_scale_length(units)
# Get script from text editor for injection
func = None
if speckle.send_script in bpy.data.texts:
mod = bpy.data.texts[speckle.send_script].as_module()
if hasattr(mod, "execute"):
func = mod.execute #type: ignore
num_converted = 0
context.window_manager.progress_begin(0, max(len(selected), 1))
depsgraph = bpy.context.evaluated_depsgraph_get() if self.apply_modifiers else None
commit_builder = BlenderCommitObjectBuilder()
for obj in selected:
try:
# Run injected function
new_object = obj
if func:
new_object = func(context.scene, obj)
if (new_object is None):
raise ConversionSkippedException(f"Script '{func.__module__}' returned None.")
converted = convert_to_speckle(
obj,
units_scale,
units,
depsgraph
)
if not converted:
raise Exception("Converter returned None")
commit_builder.include_object(converted, obj)
_report(f"Successfully converted '{obj.name_full}' as '{converted.speckle_type}'")
except ConversionSkippedException as ex:
_report(f"Skipped converting '{obj.name_full}': '{ex}'")
except Exception as ex:
_report(f"Failed to converted '{obj.name_full}': '{ex}'")
num_converted += 1
context.window_manager.progress_update(num_converted)
context.window_manager.progress_end()
commit_object = commit_builder.ensure_collection(context.scene.collection)
commit_builder.build_commit_object(commit_object)
metrics.track(
metrics.SEND,
client.account,
custom_props={
"branches": len(stream.branches),
#"collaborators": 0, #TODO:
"isMain": branch.name == "main",
},
)
_report(f"Sending data to {stream.name}")
transport = ServerTransport(stream.id, client)
OBJECT_ID = operations.send(
commit_object,
[transport],
)
COMMIT_ID = client.commit.create(
stream.id,
OBJECT_ID,
branch.name,
message=self.commit_message,
source_application="blender",
)
if client.account.serverInfo.frontend2:
sent_url = f"{user.server_url}/projects/{stream.id}/models/{branch.id}@{COMMIT_ID}"
else:
sent_url = f"{user.server_url}/streams/{stream.id}/commits/{COMMIT_ID}"
_report(f"Commit Created {sent_url}")
bpy.ops.speckle.load_user_streams() # refresh loaded commits
context.view_layer.update()
if context.area:
context.area.tag_redraw()
class ViewStreamDataApi(bpy.types.Operator):
bl_idname = "speckle.view_stream_data_api"
bl_label = "Open Stream in Web"
bl_options = {"REGISTER", "UNDO"}
bl_description = "View the stream in the web browser"
def execute(self, context):
self.view_stream_data_api(context)
return {"FINISHED"}
def view_stream_data_api(self, context: Context) -> None:
speckle = get_speckle(context)
(user, stream) = speckle.validate_stream_selection()
client = speckle_clients[int(speckle.active_user)]
if client.account.serverInfo.frontend2:
stream_url = f"{user.server_url}/projects/{stream.id}"
else:
stream_url= f"{user.server_url}/streams/{stream.id}"
if not webbrowser.open(stream_url, new=2):
raise Exception("Failed to open stream in browser")
metrics.track(
"Connector Action",
None,
custom_props={
"name": "view_stream_data_api"
},
)
class AddStreamFromURL(bpy.types.Operator):
"""
Add / select a stream using its url
"""
bl_idname = "speckle.add_stream_from_url"
bl_label = "Add stream from URL"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add an existing stream by providing its URL"
stream_url: StringProperty(
name="Stream URL", default="https://speckle.xyz/streams/3073b96e86"
) # type: ignore
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "stream_url")
def invoke(self, context, event):
wm = context.window_manager
speckle = get_speckle(context)
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
self.add_stream_from_url(context)
return {"FINISHED"}
@staticmethod
def _get_or_add_stream(user : SpeckleUserObject, stream : Stream) -> Tuple[int, SpeckleStreamObject]:
index, b_stream = next(
((i, cast(SpeckleStreamObject, s)) for i, s in enumerate(user.streams) if s.id == stream.id),
(None, None),
)
if index is not None:
assert(b_stream)
return (index, b_stream)
add_user_stream(user, stream)
return next(
(i, cast(SpeckleStreamObject, s)) for i, s in enumerate(user.streams) if s.id == stream.id
)
def add_stream_from_url(self, context: Context) -> None:
speckle = get_speckle(context)
wrapper = StreamWrapper(self.stream_url)
user_index = next(
(i for i, u in enumerate(speckle.users) if wrapper.host in u.server_url),
None,
)
if user_index is None:
raise Exception(f"No user account credentials for {wrapper.host}, have you added your account in Manager?")
speckle.active_user = str(user_index)
user = cast(SpeckleUserObject, speckle.users[user_index])
client = speckle_clients[user_index]
stream = client.stream.get(wrapper.stream_id, branch_limit=LoadUserStreams.branch_limit, commit_limit=LoadUserStreams.commits_limit)
if not isinstance(stream, Stream):
raise SpeckleException(f"Could not get the requested stream {wrapper.stream_id}")
(index, b_stream) = self._get_or_add_stream(user, stream)
user.active_stream = index
_report(f"Selecting stream at index {index} ({b_stream.id} - {b_stream.name})")
if wrapper.branch_name:
b_index = b_stream.branches.find(wrapper.branch_name)
b_stream.branch = str(b_index if b_index != -1 else 0)
elif wrapper.commit_id:
commit = client.commit.get(wrapper.stream_id, wrapper.commit_id)
if isinstance(commit, Commit):
b_index = b_stream.branches.find(commit.branchName)
if b_index == -1:
b_index = 0
b_stream.branch = str(b_index)
c_index = b_stream.branches[b_index].commits.find(commit.id)
b_stream.branches[b_index].commit = str(c_index if c_index != -1 else 0)
# Update view layer
context.view_layer.update()
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "add_stream_from_url"
},
)
class CreateStream(bpy.types.Operator):
"""
Create new stream
"""
bl_idname = "speckle.create_stream"
bl_label = "Create stream"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create new stream"
stream_name: StringProperty(name="Stream name") # type: ignore
stream_description: StringProperty(
name="Stream description", default="This is a Blender stream."
) # type: ignore
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "stream_name")
col.prop(self, "stream_description")
def invoke(self, context, event):
wm = context.window_manager
speckle = get_speckle(context)
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
self.create_stream(context)
return {"FINISHED"}
def create_stream(self, context: Context) -> None:
speckle = get_speckle(context)
user = speckle.validate_user_selection()
client = speckle_clients[int(speckle.active_user)]
client.stream.create(
name=self.stream_name,
description=self.stream_description,
is_public=True
)
bpy.ops.speckle.load_user_streams()
user.active_stream = user.streams.find(self.stream_name)
# Update view layer
context.view_layer.update()
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "create_stream"
},
)
class DeleteStream(bpy.types.Operator):
"""
Delete stream
"""
bl_idname = "speckle.delete_stream"
bl_label = "Delete stream"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Delete selected stream permanently"
are_you_sure: BoolProperty(
name="Confirm",
default=False,
) # type: ignore
delete_collection: BoolProperty(name="Delete collection", default=False) # type: ignore
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "are_you_sure")
col.prop(self, "delete_collection")
def invoke(self, context, event):
wm = context.window_manager
speckle = get_speckle(context)
if len(speckle.users) > 0:
return wm.invoke_props_dialog(self)
return {"CANCELLED"}
def execute(self, context):
if not self.are_you_sure:
_report(f"Cancelled by user - are_you_sure was {self.are_you_sure}")
return {"CANCELLED"}
self.are_you_sure = False
self.delete_stream(context, self.delete_collection)
return {"FINISHED"}
@staticmethod
def delete_stream(context: Context, delete_collection: bool) -> None:
speckle = get_speckle(context)
(_, stream) = speckle.validate_stream_selection()
client = speckle_clients[int(speckle.active_user)]
client.stream.delete(id=stream.id)
if delete_collection:
# This may not work anymore since we changed the collection naming...
col_name = "SpeckleStream_{}_{}".format(stream.name, stream.id)
if col_name in bpy.data.collections:
collection = bpy.data.collections[col_name]
bpy.data.collections.remove(collection)
bpy.ops.speckle.load_user_streams()
context.view_layer.update()
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "delete_stream"
},
)
@deprecated
class SelectOrphanObjects(bpy.types.Operator):
"""
Select Speckle objects that don't belong to any stream
"""
bl_idname = "speckle.select_orphans"
bl_label = "Select orphaned objects"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Select Speckle objects that don't belong to any stream"
def draw(self, context):
layout = self.layout
def execute(self, context):
for o in context.scene.objects:
if (
o.speckle.stream_id
and o.speckle.stream_id not in context.scene["speckle_streams"]
):
o.select = True
else:
o.select = False
metrics.track(
"Connector Action",
custom_props={
"name": "SelectOrphanObjects"
},
)
return {"FINISHED"}
class CopyStreamId(bpy.types.Operator):
"""
Copy stream ID to clipboard
"""
bl_idname = "speckle.stream_copy_id"
bl_label = "Copy stream ID"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy stream ID to clipboard"
def execute(self, context):
self.copy_stream_id(context)
return {"FINISHED"}
def copy_stream_id(self, context) -> None:
speckle = get_speckle(context)
(_, stream) = speckle.validate_stream_selection()
bpy.context.window_manager.clipboard = stream.id
metrics.track(
"Connector Action",
custom_props={
"name": "copy_stream_id"
},
)
class CopyCommitId(bpy.types.Operator):
"""
Copy commit ID to clipboard
"""
bl_idname = "speckle.commit_copy_id"
bl_label = "Copy commit ID"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy commit ID to clipboard"
def execute(self, context):
self.copy_commit_id(context)
return {"FINISHED"}
def copy_commit_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, _, commit) = speckle.validate_commit_selection()
bpy.context.window_manager.clipboard = commit.id
metrics.track(
"Connector Action",
custom_props={
"name": "copy_commit_id"
},
)
class CopyBranchName(bpy.types.Operator):
"""
Copy branch name to clipboard
"""
bl_idname = "speckle.branch_copy_name"
bl_label = "Copy branch name"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy branch name to clipboard"
def execute(self, context):
self.copy_branch_id(context)
return {"FINISHED"}
def copy_branch_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, branch) = speckle.validate_branch_selection()
bpy.context.window_manager.clipboard = branch.name
metrics.track(
"Connector Action",
custom_props={
"name": "copy_branch_id"
},
)
+221
View File
@@ -0,0 +1,221 @@
"""
User account operators
"""
from typing import List, cast
import bpy
from bpy.types import Context
from bpy_speckle.functions import _report
from bpy_speckle.clients import speckle_clients
from bpy_speckle.properties.scene import SpeckleBranchObject, SpeckleCommitObject, SpeckleSceneSettings, SpeckleStreamObject, SpeckleUserObject, get_speckle
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models import Stream
from specklepy.core.api.credentials import get_local_accounts, Account
from specklepy.logging import metrics
class ResetUsers(bpy.types.Operator):
"""
Reset loaded users
"""
bl_idname = "speckle.users_reset"
bl_label = "Reset users"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
self.reset_ui(context)
metrics.track(
"Connector Action",
None,
custom_props={
"name": "ResetUsers"
},
)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
return {"FINISHED"}
@staticmethod
def reset_ui(context: Context):
speckle = get_speckle(context)
speckle.users.clear()
speckle_clients.clear()
class LoadUsers(bpy.types.Operator):
"""
Load all users from local user database
"""
bl_idname = "speckle.users_load"
bl_label = "Load users"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
_report("Loading users...")
speckle = get_speckle(context)
users_list = speckle.users
ResetUsers.reset_ui(context)
profiles = get_local_accounts()
active_user_index = 0
metrics.track(
"Connector Action",
None,
custom_props={
"name": "LoadUsers",
},
)
if not profiles:
raise Exception("Zero accounts were found, please add one through Speckle Manager or a local account")
for profile in profiles:
try:
add_user_account(profile, speckle)
except Exception as ex:
_report(f"Failed to authenticate user account {profile.userInfo.email} with server {profile.serverInfo.url}: {ex}")
users_list.remove(len(users_list) - 1)
continue
if profile.isDefault:
active_user_index = len(users_list) - 1
_report(f"Authenticated {len(users_list)}/{len(profiles)} accounts")
if active_user_index < len(users_list):
speckle.active_user = str(active_user_index)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
if not users_list:
raise Exception("Zero valid user accounts were found, please ensure account is valid and the server is running")
return {"FINISHED"}
def add_user_account(account: Account, speckle: SpeckleSceneSettings) -> SpeckleUserObject:
"""Creates a new new SpeckleUserObject for the provided user Account and adds it to the SpeckleSceneSettings"""
users_list = speckle.users
URL = account.serverInfo.url
user = cast(SpeckleUserObject, users_list.add())
user.server_name = account.serverInfo.name or "Speckle Server"
user.server_url = URL
user.id = account.userInfo.id
user.name = account.userInfo.name
user.email = account.userInfo.email
user.company = account.userInfo.company or ""
assert(URL)
client = SpeckleClient(
host=URL,
use_ssl="https" in URL,
)
client.authenticate_with_account(account)
speckle_clients.append(client)
return user
def add_user_stream(user: SpeckleUserObject, stream: Stream):
"""Adds the provided Stream (with branch & commits) to the SpeckleUserObject"""
s = cast(SpeckleStreamObject, user.streams.add())
s.name = stream.name
s.id = stream.id
s.description = stream.description
_report(f"Adding stream {s.id} - {s.name}")
if not stream.branches:
return
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
for b in stream.branches.items:
branch = cast(SpeckleBranchObject, s.branches.add())
branch.name = b.name
branch.id = b.id
branch.description = b.description or ""
if not b.commits:
continue
for c in b.commits.items:
commit: SpeckleCommitObject = branch.commits.add()
commit.id = commit.name = c.id
commit.message = c.message or ""
commit.author_name = c.authorName
commit.author_id = c.authorId
commit.created_at = c.createdAt.strftime("%Y-%m-%d %H:%M:%S.%f%Z") if c.createdAt else ""
commit.source_application = str(c.sourceApplication)
commit.referenced_object = c.referencedObject
if hasattr(s, "baseProperties"):
s.units = stream.baseProperties.units # type: ignore
else:
s.units = "Meters"
class LoadUserStreams(bpy.types.Operator):
"""
Load all available streams for active user
"""
bl_idname = "speckle.load_user_streams"
bl_label = "Load user streams"
bl_options = {"REGISTER", "UNDO"}
bl_description = "(Re)load all available user streams"
stream_limit: int = 20
branch_limit: int = 100
commits_limit: int = 10
def execute(self, context):
self.load_user_stream(context)
return {"FINISHED"}
def load_user_stream(self, context: Context) -> None:
speckle = get_speckle(context)
user = speckle.validate_user_selection()
client = speckle_clients[int(speckle.active_user)]
try:
streams = client.stream.list(stream_limit=self.stream_limit)
except Exception as ex:
raise Exception(f"Failed to retrieve streams") from ex
if not streams:
_report("Zero streams found")
return
user.streams.clear()
for s in streams:
assert(s.id)
sstream = client.stream.get(id=s.id, branch_limit=self.branch_limit, commit_limit=10)
add_user_stream(user, sstream)
bpy.context.view_layer.update()
if context.area:
context.area.tag_redraw()
metrics.track(
"Connector Action",
client.account,
custom_props={
"name": "LoadUserStreams"
},
)
+23
View File
@@ -0,0 +1,23 @@
from .scene import (
SpeckleSceneSettings,
SpeckleSceneObject,
SpeckleUserObject,
SpeckleStreamObject,
SpeckleBranchObject,
SpeckleCommitObject,
)
from .object import SpeckleObjectSettings
from .collection import SpeckleCollectionSettings
from .addon import SpeckleAddonPreferences
property_classes = [
SpeckleSceneObject,
SpeckleCommitObject,
SpeckleBranchObject,
SpeckleStreamObject,
SpeckleUserObject,
SpeckleSceneSettings,
SpeckleObjectSettings,
SpeckleCollectionSettings,
SpeckleAddonPreferences,
]
+17
View File
@@ -0,0 +1,17 @@
"""
Addon properties
"""
import bpy
class SpeckleAddonPreferences(bpy.types.AddonPreferences):
"""
Add-on preferences
TODO: add any preferences that might be relevant here
"""
bl_idname = __package__
def draw(self, context):
layout = self.layout
layout.label(text="SpeckleBlender preferences")
+19
View File
@@ -0,0 +1,19 @@
"""
Collection properties
"""
import bpy
class SpeckleCollectionSettings(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
send_or_receive: bpy.props.EnumProperty(
name="Mode",
items=(
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
)
stream_id: bpy.props.StringProperty(default="")
name: bpy.props.StringProperty(default="")
units: bpy.props.StringProperty(default="")
+18
View File
@@ -0,0 +1,18 @@
"""
Object properties
"""
import bpy
class SpeckleObjectSettings(bpy.types.PropertyGroup):
enabled: bpy.props.BoolProperty(default=False, name="Enabled")
send_or_receive: bpy.props.EnumProperty(
name="Mode",
items=(
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
)
stream_id: bpy.props.StringProperty(default="")
object_id: bpy.props.StringProperty(default="")
+198
View File
@@ -0,0 +1,198 @@
"""
Scene properties
"""
from typing import Optional, Tuple
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
IntProperty,
PointerProperty,
)
class SpeckleSceneObject(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(default="")
class SpeckleCommitObject(bpy.types.PropertyGroup):
id: StringProperty(default="")
message: StringProperty(default="")
author_name: StringProperty(default="")
author_id: StringProperty(default="")
created_at: StringProperty(default="")
source_application: StringProperty(default="")
referenced_object: StringProperty(default="")
class SpeckleBranchObject(bpy.types.PropertyGroup):
def get_commits(self, context):
if self.commits != None and len(self.commits) > 0:
return [
(str(i), commit.id, commit.message, i)
for i, commit in enumerate(self.commits)
]
return [("0", "<none>", "<none>", 0)]
name: StringProperty(default="main")
id: StringProperty(default="")
description: StringProperty(default="")
commits: CollectionProperty(type=SpeckleCommitObject)
commit: EnumProperty(
name="Commit",
description="Active commit",
items=get_commits,
)
def get_active_commit(self) -> Optional[SpeckleCommitObject]:
selected_index = int(self.commit)
if 0 <= selected_index < len(self.commits):
return self.commits[selected_index]
return None
class SpeckleStreamObject(bpy.types.PropertyGroup):
def get_branches(self, context):
if self.branches:
return [
(str(i), branch.name, branch.name, i)
for i, branch in enumerate(self.branches)
if branch.name != "globals"
]
return [("0", "<none>", "<none>", 0)]
name: StringProperty(default="SpeckleStream")
description: StringProperty(default="No description provided.")
id: StringProperty(default="")
units: StringProperty(default="Meters")
query: StringProperty(default="")
branches: CollectionProperty(type=SpeckleBranchObject)
branch: EnumProperty(
name="Branch",
description="Active branch",
items=get_branches,
)
def get_active_branch(self) -> Optional[SpeckleBranchObject]:
selected_index = int(self.branch)
if 0 <= selected_index < len(self.branches):
return self.branches[selected_index]
return None
class SpeckleUserObject(bpy.types.PropertyGroup):
server_name: StringProperty(default="SpeckleXYZ")
server_url: StringProperty(default="https://speckle.xyz")
id: StringProperty(default="")
name: StringProperty(default="Speckle User")
email: StringProperty(default="user@speckle.xyz")
company: StringProperty(default="SpeckleSystems")
streams: CollectionProperty(type=SpeckleStreamObject)
active_stream: IntProperty(default=0)
def get_active_stream(self) -> Optional[SpeckleStreamObject]:
selected_index = int(self.active_stream)
if 0 <= selected_index < len(self.streams):
return self.streams[selected_index]
return None
class SpeckleSceneSettings(bpy.types.PropertyGroup):
def get_scripts(self, context):
return [
("<none>", "<none>", "<none>"),
*[(t.name, t.name, t.name) for t in bpy.data.texts],
]
streams: EnumProperty(
name="Available streams",
description="Available streams associated with user.",
items=[],
)
users: CollectionProperty(type=SpeckleUserObject)
def get_users(self, context):
return [
(str(i), "{} ({})".format(user.email, user.server_name), user.server_url, i)
for i, user in enumerate(self.users)
]
def set_user(self, context):
bpy.ops.speckle.load_user_streams()
active_user: EnumProperty(
items=get_users,
name="Account",
description="Select account",
update=set_user,
get=None,
set=None,
)
objects: CollectionProperty(type=SpeckleSceneObject)
scale: FloatProperty(default=0.001)
user: StringProperty(
name="User",
description="Current user.",
default="Speckle User",
)
receive_script: EnumProperty(
name="Receive script",
description="Script to run when receiving stream objects.",
items=get_scripts,
)
send_script: EnumProperty(
name="Send script",
description="Script to run when sending stream objects.",
items=get_scripts,
)
def get_active_user(self) -> Optional[SpeckleUserObject]:
selected_index = int(self.active_user)
if 0 <= selected_index < len(self.users):
return self.users[selected_index]
return None
def validate_user_selection(self) -> SpeckleUserObject:
user = self.get_active_user()
if not user:
raise SelectionException("No user selected/found")
return user
def validate_stream_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject]:
user = self.validate_user_selection()
stream = user.get_active_stream()
if not stream:
raise SelectionException("No stream selected/found")
return (user, stream)
def validate_branch_selection(self) -> Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject]:
(user, stream) = self.validate_stream_selection()
branch = stream.get_active_branch()
if not branch:
raise SelectionException("No branch selected/found")
return (user, stream, branch)
def validate_commit_selection(self) ->Tuple[SpeckleUserObject, SpeckleStreamObject, SpeckleBranchObject, SpeckleCommitObject]:
(user, stream, branch) = self.validate_branch_selection()
commit = branch.get_active_commit()
if commit is None:
raise SelectionException("No commit selected/found")
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
return context.scene.speckle #type: ignore
+18
View File
@@ -0,0 +1,18 @@
from .object import OBJECT_PT_speckle
from .view3d import (
VIEW3D_UL_SpeckleUsers,
VIEW3D_UL_SpeckleStreams,
VIEW3D_PT_SpeckleUser,
VIEW3D_PT_SpeckleStreams,
VIEW3D_PT_SpeckleActiveStream,
VIEW3D_PT_SpeckleHelp,
)
ui_classes = [
VIEW3D_PT_SpeckleUser,
VIEW3D_PT_SpeckleStreams,
VIEW3D_PT_SpeckleActiveStream,
VIEW3D_UL_SpeckleUsers,
VIEW3D_UL_SpeckleStreams,
VIEW3D_PT_SpeckleHelp,
]
+36
View File
@@ -0,0 +1,36 @@
"""
Object UI elements
"""
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from deprecated import deprecated
@deprecated
class OBJECT_PT_speckle(bpy.types.Panel):
bl_space_type = "PROPERTIES"
# bl_idname = 'OBJECT_PT_speckle'
bl_region_type = "WINDOW"
bl_context = "object"
bl_label = "Speckle"
def draw_header(self, context):
self.layout.prop(context.object.speckle, "enabled", text="")
def draw(self, context):
ob = context.object
layout = self.layout
layout.active = ob.speckle.enabled
col = layout.column()
col.prop(ob.speckle, "send_or_receive", expand=True)
col.prop(ob.speckle, "stream_id", text="Stream ID")
col.prop(ob.speckle, "object_id", text="Object ID")
col.operator("speckle.update_object", text="Update")
col.operator("speckle.reset_object", text="Reset")
col.operator("speckle.delete_object", text="Delete")
+271
View File
@@ -0,0 +1,271 @@
"""
Speckle UI elements for the 3d viewport
"""
import bpy
from bpy.props import (
StringProperty,
BoolProperty,
FloatProperty,
CollectionProperty,
EnumProperty,
)
from datetime import datetime
"""
Compatibility
TODO: evaluate if we should still support Blender <2.80
"""
Region = "TOOLS" if bpy.app.version < (2, 80, 0) else "UI"
def wrap(width, text):
"""
Split strings into width for
wrapping
"""
lines = []
arr = text.split()
lengthSum = 0
line = []
for var in arr:
lengthSum += len(var) + 1
if lengthSum <= width:
line.append(var)
else:
lines.append(" ".join(line))
line = [var]
lengthSum = len(var)
lines.append(" ".join(line))
return lines
def get_available_users(self, context):
"""
Function to populate users list
"""
return [(a, a, a.name) for a in context.scene.speckle.users]
class VIEW3D_UL_SpeckleUsers(bpy.types.UIList):
"""
Speckle user list
"""
def draw_item(self, context, layout, data, user, active_data, active_propname):
if self.layout_type in {"DEFAULT", "COMPACT"}:
if user:
# layout.prop(user, "name", text=user.name, emboss=False, icon_value=0)
layout.label(
text=user.name + " (" + user.email + ")",
translate=False,
icon_value=0,
)
else:
layout.label(text="", translate=False, icon_value=0)
elif self.layout_type in {"GRID"}:
layout.alignment = "CENTER"
layout.label(text="Users", icon_value=0)
class VIEW3D_UL_SpeckleStreams(bpy.types.UIList):
"""
Speckle stream list
"""
def draw_item(self, context, layout, data, stream, active_data, active_propname):
if self.layout_type in {"DEFAULT", "COMPACT"}:
if stream:
layout.label(
text=f"{stream.name} ({stream.id})",
translate=False,
icon_value=0,
)
else:
layout.label(text=" ", translate=False, icon_value=0)
elif self.layout_type in {"GRID"}:
layout.alignment = "CENTER"
layout.label(text="Streams", icon_value=0)
class VIEW3D_PT_SpeckleUser(bpy.types.Panel):
"""
Speckle Users UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "User"
def draw(self, context):
speckle = context.scene.speckle
layout = self.layout
col = layout.column()
if len(speckle.users) < 1:
col.label(text="Refresh to initialise")
else:
col.prop(speckle, "active_user", text="")
user = speckle.users[int(speckle.active_user)]
col.label(text="{} ({})".format(user.server_name, user.server_url))
col.label(text="{} ({})".format(user.name, user.email))
col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
"""
Speckle Streams UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Streams"
def draw(self, context):
speckle = context.scene.speckle
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No stream data.")
else:
user = speckle.users[int(speckle.active_user)]
col.template_list(
"VIEW3D_UL_SpeckleStreams", "", user, "streams", user, "active_stream"
)
row = col.row(align=True)
row.operator("speckle.add_stream_from_url", text="", icon="URL")
row.operator("speckle.create_stream", text="", icon="ADD")
row.operator("speckle.delete_stream", text="", icon="REMOVE")
row.operator("speckle.load_user_streams", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
"""
Speckle Active Streams UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Active stream"
def draw(self, context):
speckle = context.scene.speckle
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No stream data.")
else:
user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
col.label(text="No active stream.")
else:
stream = user.streams[user.active_stream]
# user.active_stream = min(user.active_stream, len(user.streams) - 1)
row = col.row()
row.label(text=f"{stream.name} ({stream.id})")
row.operator("speckle.stream_copy_id", text="", icon="COPY_ID")
col.separator()
row = col.row()
row.prop(stream, "branch", text="")
row.operator("speckle.branch_copy_name", text="", icon="COPY_ID")
if len(stream.branches) > 0:
branch = stream.branches[int(stream.branch)]
row = col.row()
row.prop(branch, "commit", text="")
row.operator("speckle.commit_copy_id", text="", icon="COPY_ID")
if len(branch.commits) > 0:
commit = branch.commits[int(branch.commit)]
area = col.box()
area.separator()
lines = wrap(32, commit.message)
for line in lines:
row = area.row(align=True)
row.alignment = "EXPAND"
row.scale_y = 0.4
row.label(text=line)
area.separator()
dt = datetime.strptime(
commit.created_at, "%Y-%m-%d %H:%M:%S.%f%Z"
)
col.label(text=f"{dt.ctime()}")
col.label(text=f"{commit.author_name} ({commit.author_id})")
col.label(text=commit.source_application)
else:
col.label(text="No branches found!")
col.separator()
area = col.box()
row = area.row()
subcol = row.column()
subcol.operator("speckle.receive_stream_objects", text="Receive")
subcol.prop(speckle, "receive_script", text="")
subcol = row.column()
subcol.operator("speckle.send_stream_objects", text="Send")
subcol.prop(speckle, "send_script", text="")
area.prop(stream, "query", text="Filter")
col.separator()
row = col.row(align=True)
subcol = row.column()
col.label(text="Description:")
area = col.box()
area.separator()
lines = wrap(32, stream.description)
for line in lines:
row = area.row(align=True)
row.alignment = "EXPAND"
row.scale_y = 0.4
row.label(text=line)
area.separator()
col.separator()
col.operator("speckle.view_stream_data_api", text="Open Stream in Web")
class VIEW3D_PT_SpeckleHelp(bpy.types.Panel):
"""
Speckle Help UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle"
bl_context = "objectmode"
bl_label = "Help"
def draw(self, context):
layout = self.layout
col = layout.column()
col.operator("speckle.open_speckle_guide")
col.separator()
col.operator("speckle.open_speckle_tutorials")
col.separator()
col.operator("speckle.open_speckle_forum")
+54
View File
@@ -0,0 +1,54 @@
def find_key_case_insensitive(data, key, default=None):
value = data.get(key)
if value:
return value
"""
Necessary to find keys where the first character
is capitalized
"""
value = data.get(key[0].upper() + key[1:])
if value:
return value
value = data.get(key.upper())
if value:
return value
return default
def get_iddata(base, uuid, name, obdata):
"""
This is taken from the import_3dm add-on:
https://github.com/jesterKing/import_3dm
# Copyright (c) 2018-2019 Nathan Letwory, Joel Putnam,
Tom Svilans
Get an iddata. If an object with given uuid is found in
this .blend use that. Otherwise new up one with base.new,
potentially with obdata if that is set
"""
founditem = None
if uuid is not None:
for item in base:
if item.get("speckle_id", None) == str(uuid):
founditem = item
break
elif name:
for item in base:
if item.get("name", None) == name:
founditem = item
break
if founditem:
theitem = founditem
theitem["name"] = name
if obdata:
theitem.data = obdata
else:
if obdata:
theitem = base.new(name=name, object_data=obdata)
else:
theitem = base.new(name=name)
tag_data(theitem, uuid, name)
return theitem
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -e -o pipefail
uv pip compile pyproject.toml --output-file bpy_speckle/requirements.txt --generate-hashes
poetry export --only main -o bpy_speckle/requirements.txt
+20
View File
@@ -0,0 +1,20 @@
import sys
from pathlib import Path
def patch_installer(tag: str):
"""Patches the installer with the correct connector version and specklepy version"""
tag = tag.replace("\n", "")
iss_file = "speckle-sharp-ci-tools/blender.iss"
iss_path = Path(iss_file)
lines = iss_path.read_text().split("\n")
lines.insert(12, f'#define AppVersion "{tag.split("-")[0]}"')
lines.insert(13, f'#define AppInfoVersion "{tag}"')
iss_path.write_text("\n".join(lines))
print(f"Patched installer with connector v{tag}")
if __name__ == "__main__":
tag = sys.argv[1]
patch_installer(tag)
+10 -32
View File
@@ -1,52 +1,30 @@
import re
import sys
def patch_addon(simple_version: str):
"""Patches the __init__.py bl_info version within the connector init file"""
FILE_PATH = "bpy_speckle/__init__.py"
version = simple_version.split(".")
with open(FILE_PATH, "r") as file:
lines = file.readlines()
for index, line in enumerate(lines):
if '"version":' in line:
lines[index] = (
f' "version": ({version[0]}, {version[1]}, {version[2]}),\n'
)
with open(FILE_PATH, "w") as file:
file.writelines(lines)
def patch_manifest(simple_version: str):
def patch_connector(tag):
"""Patches the connector version within the connector init file"""
FILE_PATH = "bpy_speckle/blender_manifest.toml"
version = simple_version.split(".")
bpy_file = "bpy_speckle/__init__.py"
tag = tag.split(".")
with open(FILE_PATH, "r") as file:
with open(bpy_file, "r") as file:
lines = file.readlines()
for index, line in enumerate(lines):
if line.startswith("version ="):
lines[index] = f'version = "{version[0]}.{version[1]}.{version[2]}",\n'
print(f"Patched connector version number in {FILE_PATH}")
for (index, line) in enumerate(lines):
if '"version":' in line:
lines[index] = f' "version": ({tag[0]}, {tag[1]}, {tag[2]}),\n'
print(f"Patched connector version number in {bpy_file}")
break
with open(FILE_PATH, "w") as file:
with open(bpy_file, "w") as file:
file.writelines(lines)
def main():
tag = sys.argv[1]
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
raise ValueError(f"Invalid tag provided: {tag}")
print(f"Patching version: {tag}")
simple_version = tag.split("-")[0]
patch_addon(simple_version)
patch_manifest(simple_version)
patch_connector(tag.split("-")[0])
if __name__ == "__main__":
Generated
+1289
View File
File diff suppressed because it is too large Load Diff
+20 -12
View File
@@ -1,16 +1,24 @@
[project]
[tool.poetry]
name = "speckle-blender"
version = "3.0.0"
description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.11.9, <4.0.0"
version = "2.0.0"
description = "the Speckle 2.0 connector for Blender!"
authors = ["izzy lyseggen <izzy.lyseggen@gmail.com>", "Gergő Jedlicska <gergo@jedlicska.com>"]
license = "Apache-2.0"
dependencies = [
"specklepy>=3.0.3",
]
[dependency-groups]
dev = [
"fake-bpy-module-latest>=20240524,<20240525",
"ruff>=0.4.4,<0.5",
]
[tool.poetry.dependencies]
python = ">=3.8, <4.0.0"
specklepy = "^2.18.0"
attrs = "^23.1.0"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
fake-bpy-module-latest = "^20240212"
black = "23.11.0"
pylint = "^2.15.7"
ruff = "^0.0.187"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Generated
-788
View File
@@ -1,788 +0,0 @@
version = 1
revision = 1
requires-python = ">=3.11.9, <4.0.0"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
[[package]]
name = "appdirs"
version = "1.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566 },
]
[[package]]
name = "attrs"
version = "25.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
]
[[package]]
name = "backoff"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 },
]
[[package]]
name = "certifi"
version = "2025.4.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 },
{ url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 },
{ url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 },
{ url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 },
{ url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 },
{ url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 },
{ url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 },
{ url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 },
{ url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 },
{ url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 },
{ url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 },
{ url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 },
{ url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 },
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
]
[[package]]
name = "deprecated"
version = "1.2.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 },
]
[[package]]
name = "fake-bpy-module-latest"
version = "20240524"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/d9/f92ba292561805c06eb688ea8eb3c44a8c519bc6a092d040084582809e98/fake_bpy_module_latest-20240524.tar.gz", hash = "sha256:752da840cf6e69b1e8898382a89b2107a98dc6cb45287d44ac32be1176f09bed", size = 967498 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/16/c2cb7912fd1ccc13a57ab43587ab4c97e3227ed15b7f890431031e31a1bd/fake_bpy_module_latest-20240524-py3-none-any.whl", hash = "sha256:909756548ac8d6fcdc647082442d0c544f872c55d9884ec85301d69e79837688", size = 1200494 },
]
[[package]]
name = "gql"
version = "3.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "backoff" },
{ name = "graphql-core" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/ed/44ffd30b06b3afc8274ee2f38c3c1b61fe4740bf03d92083e43d2c17ac77/gql-3.5.3.tar.gz", hash = "sha256:393b8c049d58e0d2f5461b9d738a2b5f904186a40395500b4a84dd092d56e42b", size = 180504 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/50/2f4e99b216821ac921dbebf91c644ba95818f5d07857acadee17220221f3/gql-3.5.3-py2.py3-none-any.whl", hash = "sha256:e1fcbde2893fcafdd28114ece87ff47f1cc339a31db271fc4e1d528f5a1d4fbc", size = 74348 },
]
[package.optional-dependencies]
requests = [
{ name = "requests" },
{ name = "requests-toolbelt" },
]
websockets = [
{ name = "websockets" },
]
[[package]]
name = "graphql-core"
version = "3.2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416 },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "multidict"
version = "6.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/1b/4c6e638195851524a63972c5773c7737bea7e47b1ba402186a37773acee2/multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95", size = 65515 },
{ url = "https://files.pythonhosted.org/packages/25/d5/10e6bca9a44b8af3c7f920743e5fc0c2bcf8c11bf7a295d4cfe00b08fb46/multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a", size = 38609 },
{ url = "https://files.pythonhosted.org/packages/26/b4/91fead447ccff56247edc7f0535fbf140733ae25187a33621771ee598a18/multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223", size = 37871 },
{ url = "https://files.pythonhosted.org/packages/3b/37/cbc977cae59277e99d15bbda84cc53b5e0c4929ffd91d958347200a42ad0/multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44", size = 226661 },
{ url = "https://files.pythonhosted.org/packages/15/cd/7e0b57fbd4dc2fc105169c4ecce5be1a63970f23bb4ec8c721b67e11953d/multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065", size = 223422 },
{ url = "https://files.pythonhosted.org/packages/f1/01/1de268da121bac9f93242e30cd3286f6a819e5f0b8896511162d6ed4bf8d/multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f", size = 235447 },
{ url = "https://files.pythonhosted.org/packages/d2/8c/8b9a5e4aaaf4f2de14e86181a3a3d7b105077f668b6a06f043ec794f684c/multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a", size = 231455 },
{ url = "https://files.pythonhosted.org/packages/35/db/e1817dcbaa10b319c412769cf999b1016890849245d38905b73e9c286862/multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2", size = 223666 },
{ url = "https://files.pythonhosted.org/packages/4a/e1/66e8579290ade8a00e0126b3d9a93029033ffd84f0e697d457ed1814d0fc/multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1", size = 217392 },
{ url = "https://files.pythonhosted.org/packages/7b/6f/f8639326069c24a48c7747c2a5485d37847e142a3f741ff3340c88060a9a/multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42", size = 228969 },
{ url = "https://files.pythonhosted.org/packages/d2/c3/3d58182f76b960eeade51c89fcdce450f93379340457a328e132e2f8f9ed/multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e", size = 217433 },
{ url = "https://files.pythonhosted.org/packages/e1/4b/f31a562906f3bd375f3d0e83ce314e4a660c01b16c2923e8229b53fba5d7/multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd", size = 225418 },
{ url = "https://files.pythonhosted.org/packages/99/89/78bb95c89c496d64b5798434a3deee21996114d4d2c28dd65850bf3a691e/multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925", size = 235042 },
{ url = "https://files.pythonhosted.org/packages/74/91/8780a6e5885a8770442a8f80db86a0887c4becca0e5a2282ba2cae702bc4/multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c", size = 230280 },
{ url = "https://files.pythonhosted.org/packages/68/c1/fcf69cabd542eb6f4b892469e033567ee6991d361d77abdc55e3a0f48349/multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08", size = 223322 },
{ url = "https://files.pythonhosted.org/packages/b8/85/5b80bf4b83d8141bd763e1d99142a9cdfd0db83f0739b4797172a4508014/multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49", size = 35070 },
{ url = "https://files.pythonhosted.org/packages/09/66/0bed198ffd590ab86e001f7fa46b740d58cf8ff98c2f254e4a36bf8861ad/multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529", size = 38667 },
{ url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293 },
{ url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096 },
{ url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214 },
{ url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686 },
{ url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061 },
{ url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412 },
{ url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563 },
{ url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811 },
{ url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524 },
{ url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012 },
{ url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765 },
{ url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888 },
{ url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041 },
{ url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046 },
{ url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106 },
{ url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351 },
{ url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791 },
{ url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123 },
{ url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049 },
{ url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078 },
{ url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097 },
{ url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768 },
{ url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331 },
{ url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169 },
{ url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947 },
{ url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761 },
{ url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605 },
{ url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144 },
{ url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100 },
{ url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731 },
{ url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637 },
{ url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594 },
{ url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359 },
{ url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903 },
{ url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895 },
{ url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183 },
{ url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592 },
{ url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071 },
{ url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597 },
{ url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253 },
{ url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146 },
{ url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585 },
{ url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080 },
{ url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558 },
{ url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168 },
{ url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970 },
{ url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980 },
{ url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641 },
{ url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728 },
{ url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913 },
{ url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112 },
{ url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481 },
]
[[package]]
name = "propcache"
version = "0.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207 },
{ url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648 },
{ url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496 },
{ url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288 },
{ url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456 },
{ url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429 },
{ url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472 },
{ url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480 },
{ url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530 },
{ url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230 },
{ url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754 },
{ url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430 },
{ url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884 },
{ url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480 },
{ url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757 },
{ url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500 },
{ url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674 },
{ url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570 },
{ url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094 },
{ url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958 },
{ url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894 },
{ url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672 },
{ url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395 },
{ url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510 },
{ url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949 },
{ url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258 },
{ url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036 },
{ url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684 },
{ url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562 },
{ url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142 },
{ url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711 },
{ url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479 },
{ url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286 },
{ url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425 },
{ url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846 },
{ url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871 },
{ url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720 },
{ url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203 },
{ url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365 },
{ url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016 },
{ url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596 },
{ url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977 },
{ url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220 },
{ url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642 },
{ url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789 },
{ url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880 },
{ url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220 },
{ url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678 },
{ url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560 },
{ url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676 },
{ url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701 },
{ url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934 },
{ url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316 },
{ url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619 },
{ url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896 },
{ url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111 },
{ url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334 },
{ url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026 },
{ url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724 },
{ url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868 },
{ url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322 },
{ url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778 },
{ url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175 },
{ url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857 },
{ url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663 },
]
[[package]]
name = "pydantic"
version = "2.11.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 },
]
[[package]]
name = "pydantic-core"
version = "2.33.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 },
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 },
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 },
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 },
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 },
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 },
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 },
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 },
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 },
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 },
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 },
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 },
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 },
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 },
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 },
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 },
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 },
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 },
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 },
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 },
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 },
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 },
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 },
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 },
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 },
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 },
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 },
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 },
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 },
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 },
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 },
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 },
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 },
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 },
]
[[package]]
name = "pydantic-settings"
version = "2.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
]
[[package]]
name = "python-dotenv"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 },
]
[[package]]
name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 },
]
[[package]]
name = "ruff"
version = "0.4.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ea/04/b660bc832ebfa40e1788edf6934388340751cbc6f733d1f807edca9d96e6/ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804", size = 2577674 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/53/0d/134fdd72f566d37b0c59b6e55f60993c705f93a0fe3c1faa6f8a269057c7/ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac", size = 8510271 },
{ url = "https://files.pythonhosted.org/packages/46/5e/4ac799ffec39ef5012052c1f144a0f7a63a0322ebd328b802d64beb3d091/ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e", size = 8107776 },
{ url = "https://files.pythonhosted.org/packages/78/6f/37af054d3ced5a6196201f6c248eeaec6b3b844136cf3da510d591dbfd89/ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6", size = 9868358 },
{ url = "https://files.pythonhosted.org/packages/c7/38/070baf0393ba0da9d85409bdd63874776926acfc372e8e9f0ed21957aeee/ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784", size = 9172824 },
{ url = "https://files.pythonhosted.org/packages/e7/9d/bad51d81c918e1ce1648b24480a63f5605662efe69b55fad05825b5711ff/ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739", size = 9997887 },
{ url = "https://files.pythonhosted.org/packages/ec/a4/1310b3d003cb67f3c86cb8cc5c5e475dab152b1eef88558abd11e55daaad/ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81", size = 10743762 },
{ url = "https://files.pythonhosted.org/packages/b8/c1/5373bc5a4c3782c0a368ce5ca4ec3a689574daf71f68f55720a6a64321d4/ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d", size = 10329524 },
{ url = "https://files.pythonhosted.org/packages/48/dc/2c057e7717a3eaaa89ea848a26ef085930a2509f9b66ceae55319668c03d/ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e", size = 11208593 },
{ url = "https://files.pythonhosted.org/packages/11/c3/3f89b1e967a869642bd9198f27e2b89b8300862555d3e1e39b4ccaf92e8b/ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6", size = 10041835 },
{ url = "https://files.pythonhosted.org/packages/d0/e6/734aed23112de8df5a2f3bc02e9e45cd3910fe83b0d2bb2456e200c52d98/ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631", size = 9842683 },
{ url = "https://files.pythonhosted.org/packages/cf/13/bc788b2e21d3e4db74d1375da22f50f944bc1fef064c4749f307b0c8794f/ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef", size = 9283929 },
{ url = "https://files.pythonhosted.org/packages/f0/09/f3c6560f9d81a4c5d800996090c9cc54d794ea14ab8f8af46b7483005963/ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815", size = 9617526 },
{ url = "https://files.pythonhosted.org/packages/d3/9e/11ae4e8587efe40aa083835665d0818626f8f4a10aa4ebc097cdbfae7624/ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695", size = 10114053 },
{ url = "https://files.pythonhosted.org/packages/e8/94/3bb62a0086e9c61d0506e546e7cf68456fd93bf569a8adfa5e324812970d/ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca", size = 7707741 },
{ url = "https://files.pythonhosted.org/packages/d8/4e/6fd32ebd0a09f25ed9911b77c5273b7a6b3b50a78d6ed0508d66a24398b8/ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7", size = 8519153 },
{ url = "https://files.pythonhosted.org/packages/dc/78/5109b7db3b44a64157b025e45eec6591e4beb53732104637d8e0ee0c5570/ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0", size = 7906942 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "speckle-blender"
version = "3.0.0"
source = { virtual = "." }
dependencies = [
{ name = "specklepy" },
]
[package.dev-dependencies]
dev = [
{ name = "fake-bpy-module-latest" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [{ name = "specklepy", specifier = ">=3.0.1" }]
[package.metadata.requires-dev]
dev = [
{ name = "fake-bpy-module-latest", specifier = ">=20240524,<20240525" },
{ name = "ruff", specifier = ">=0.4.4,<0.5" },
]
[[package]]
name = "specklepy"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "appdirs" },
{ name = "attrs" },
{ name = "deprecated" },
{ name = "gql", extra = ["requests", "websockets"] },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "ujson" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/e1/92a88f3651271177200e1c222720c125d317eb95766dd3a68548f50d4ba5/specklepy-3.0.1.tar.gz", hash = "sha256:a535e73cb378cf7dee35c60a45b48b2c4866d5216b9abcf0426d8223091d3a2e", size = 201758 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/c4/fa934c65d6cb9351da126a2a2e738c57362da415b7acc10fe43f2aa17efa/specklepy-3.0.1-py3-none-any.whl", hash = "sha256:91cd6a32c462db9969cc4872f67b97ae0c861bdb75b78aba9597ef67936aa675", size = 110323 },
]
[[package]]
name = "typing-extensions"
version = "4.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 },
]
[[package]]
name = "typing-inspection"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
]
[[package]]
name = "ujson"
version = "5.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/00/3110fd566786bfa542adb7932d62035e0c0ef662a8ff6544b6643b3d6fd7/ujson-5.10.0.tar.gz", hash = "sha256:b3cd8f3c5d8c7738257f1018880444f7b7d9b66232c64649f562d7ba86ad4bc1", size = 7154885 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/ec/3c551ecfe048bcb3948725251fb0214b5844a12aa60bee08d78315bb1c39/ujson-5.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a5b366812c90e69d0f379a53648be10a5db38f9d4ad212b60af00bd4048d0f00", size = 55353 },
{ url = "https://files.pythonhosted.org/packages/8d/9f/4731ef0671a0653e9f5ba18db7c4596d8ecbf80c7922dd5fe4150f1aea76/ujson-5.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:502bf475781e8167f0f9d0e41cd32879d120a524b22358e7f205294224c71126", size = 51813 },
{ url = "https://files.pythonhosted.org/packages/1f/2b/44d6b9c1688330bf011f9abfdb08911a9dc74f76926dde74e718d87600da/ujson-5.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b91b5d0d9d283e085e821651184a647699430705b15bf274c7896f23fe9c9d8", size = 51988 },
{ url = "https://files.pythonhosted.org/packages/29/45/f5f5667427c1ec3383478092a414063ddd0dfbebbcc533538fe37068a0a3/ujson-5.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:129e39af3a6d85b9c26d5577169c21d53821d8cf68e079060602e861c6e5da1b", size = 53561 },
{ url = "https://files.pythonhosted.org/packages/26/21/a0c265cda4dd225ec1be595f844661732c13560ad06378760036fc622587/ujson-5.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f77b74475c462cb8b88680471193064d3e715c7c6074b1c8c412cb526466efe9", size = 58497 },
{ url = "https://files.pythonhosted.org/packages/28/36/8fde862094fd2342ccc427a6a8584fed294055fdee341661c78660f7aef3/ujson-5.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ec0ca8c415e81aa4123501fee7f761abf4b7f386aad348501a26940beb1860f", size = 997877 },
{ url = "https://files.pythonhosted.org/packages/90/37/9208e40d53baa6da9b6a1c719e0670c3f474c8fc7cc2f1e939ec21c1bc93/ujson-5.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab13a2a9e0b2865a6c6db9271f4b46af1c7476bfd51af1f64585e919b7c07fd4", size = 1140632 },
{ url = "https://files.pythonhosted.org/packages/89/d5/2626c87c59802863d44d19e35ad16b7e658e4ac190b0dead17ff25460b4c/ujson-5.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57aaf98b92d72fc70886b5a0e1a1ca52c2320377360341715dd3933a18e827b1", size = 1043513 },
{ url = "https://files.pythonhosted.org/packages/2f/ee/03662ce9b3f16855770f0d70f10f0978ba6210805aa310c4eebe66d36476/ujson-5.10.0-cp311-cp311-win32.whl", hash = "sha256:2987713a490ceb27edff77fb184ed09acdc565db700ee852823c3dc3cffe455f", size = 38616 },
{ url = "https://files.pythonhosted.org/packages/3e/20/952dbed5895835ea0b82e81a7be4ebb83f93b079d4d1ead93fcddb3075af/ujson-5.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:f00ea7e00447918ee0eff2422c4add4c5752b1b60e88fcb3c067d4a21049a720", size = 42071 },
{ url = "https://files.pythonhosted.org/packages/e8/a6/fd3f8bbd80842267e2d06c3583279555e8354c5986c952385199d57a5b6c/ujson-5.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98ba15d8cbc481ce55695beee9f063189dce91a4b08bc1d03e7f0152cd4bbdd5", size = 55642 },
{ url = "https://files.pythonhosted.org/packages/a8/47/dd03fd2b5ae727e16d5d18919b383959c6d269c7b948a380fdd879518640/ujson-5.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9d2edbf1556e4f56e50fab7d8ff993dbad7f54bac68eacdd27a8f55f433578e", size = 51807 },
{ url = "https://files.pythonhosted.org/packages/25/23/079a4cc6fd7e2655a473ed9e776ddbb7144e27f04e8fc484a0fb45fe6f71/ujson-5.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6627029ae4f52d0e1a2451768c2c37c0c814ffc04f796eb36244cf16b8e57043", size = 51972 },
{ url = "https://files.pythonhosted.org/packages/04/81/668707e5f2177791869b624be4c06fb2473bf97ee33296b18d1cf3092af7/ujson-5.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ccb77b3e40b151e20519c6ae6d89bfe3f4c14e8e210d910287f778368bb3d1", size = 53686 },
{ url = "https://files.pythonhosted.org/packages/bd/50/056d518a386d80aaf4505ccf3cee1c40d312a46901ed494d5711dd939bc3/ujson-5.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3caf9cd64abfeb11a3b661329085c5e167abbe15256b3b68cb5d914ba7396f3", size = 58591 },
{ url = "https://files.pythonhosted.org/packages/fc/d6/aeaf3e2d6fb1f4cfb6bf25f454d60490ed8146ddc0600fae44bfe7eb5a72/ujson-5.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6e32abdce572e3a8c3d02c886c704a38a1b015a1fb858004e03d20ca7cecbb21", size = 997853 },
{ url = "https://files.pythonhosted.org/packages/f8/d5/1f2a5d2699f447f7d990334ca96e90065ea7f99b142ce96e85f26d7e78e2/ujson-5.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a65b6af4d903103ee7b6f4f5b85f1bfd0c90ba4eeac6421aae436c9988aa64a2", size = 1140689 },
{ url = "https://files.pythonhosted.org/packages/f2/2c/6990f4ccb41ed93744aaaa3786394bca0875503f97690622f3cafc0adfde/ujson-5.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:604a046d966457b6cdcacc5aa2ec5314f0e8c42bae52842c1e6fa02ea4bda42e", size = 1043576 },
{ url = "https://files.pythonhosted.org/packages/14/f5/a2368463dbb09fbdbf6a696062d0c0f62e4ae6fa65f38f829611da2e8fdd/ujson-5.10.0-cp312-cp312-win32.whl", hash = "sha256:6dea1c8b4fc921bf78a8ff00bbd2bfe166345f5536c510671bccececb187c80e", size = 38764 },
{ url = "https://files.pythonhosted.org/packages/59/2d/691f741ffd72b6c84438a93749ac57bf1a3f217ac4b0ea4fd0e96119e118/ujson-5.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:38665e7d8290188b1e0d57d584eb8110951a9591363316dd41cf8686ab1d0abc", size = 42211 },
{ url = "https://files.pythonhosted.org/packages/0d/69/b3e3f924bb0e8820bb46671979770c5be6a7d51c77a66324cdb09f1acddb/ujson-5.10.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:618efd84dc1acbd6bff8eaa736bb6c074bfa8b8a98f55b61c38d4ca2c1f7f287", size = 55646 },
{ url = "https://files.pythonhosted.org/packages/32/8a/9b748eb543c6cabc54ebeaa1f28035b1bd09c0800235b08e85990734c41e/ujson-5.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38d5d36b4aedfe81dfe251f76c0467399d575d1395a1755de391e58985ab1c2e", size = 51806 },
{ url = "https://files.pythonhosted.org/packages/39/50/4b53ea234413b710a18b305f465b328e306ba9592e13a791a6a6b378869b/ujson-5.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67079b1f9fb29ed9a2914acf4ef6c02844b3153913eb735d4bf287ee1db6e557", size = 51975 },
{ url = "https://files.pythonhosted.org/packages/b4/9d/8061934f960cdb6dd55f0b3ceeff207fcc48c64f58b43403777ad5623d9e/ujson-5.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7d0e0ceeb8fe2468c70ec0c37b439dd554e2aa539a8a56365fd761edb418988", size = 53693 },
{ url = "https://files.pythonhosted.org/packages/f5/be/7bfa84b28519ddbb67efc8410765ca7da55e6b93aba84d97764cd5794dbc/ujson-5.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e02cd37bc7c44d587a0ba45347cc815fb7a5fe48de16bf05caa5f7d0d2e816", size = 58594 },
{ url = "https://files.pythonhosted.org/packages/48/eb/85d465abafb2c69d9699cfa5520e6e96561db787d36c677370e066c7e2e7/ujson-5.10.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a890b706b64e0065f02577bf6d8ca3b66c11a5e81fb75d757233a38c07a1f20", size = 997853 },
{ url = "https://files.pythonhosted.org/packages/9f/76/2a63409fc05d34dd7d929357b7a45e3a2c96f22b4225cd74becd2ba6c4cb/ujson-5.10.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:621e34b4632c740ecb491efc7f1fcb4f74b48ddb55e65221995e74e2d00bbff0", size = 1140694 },
{ url = "https://files.pythonhosted.org/packages/45/ed/582c4daba0f3e1688d923b5cb914ada1f9defa702df38a1916c899f7c4d1/ujson-5.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9500e61fce0cfc86168b248104e954fead61f9be213087153d272e817ec7b4f", size = 1043580 },
{ url = "https://files.pythonhosted.org/packages/d7/0c/9837fece153051e19c7bade9f88f9b409e026b9525927824cdf16293b43b/ujson-5.10.0-cp313-cp313-win32.whl", hash = "sha256:4c4fc16f11ac1612f05b6f5781b384716719547e142cfd67b65d035bd85af165", size = 38766 },
{ url = "https://files.pythonhosted.org/packages/d7/72/6cb6728e2738c05bbe9bd522d6fc79f86b9a28402f38663e85a28fddd4a0/ujson-5.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:4573fd1695932d4f619928fd09d5d03d917274381649ade4328091ceca175539", size = 42212 },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
]
[[package]]
name = "websockets"
version = "11.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/3b/2ed38e52eed4cf277f9df5f0463a99199a04d9e29c9e227cfafa57bd3993/websockets-11.0.3.tar.gz", hash = "sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016", size = 104235 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/49/ae616bd221efba84a3d78737b417f704af1ffa36f40dcaba5eb954dd4753/websockets-11.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb", size = 123748 },
{ url = "https://files.pythonhosted.org/packages/0a/84/68b848a373493b58615d6c10e9e8ccbaadfd540f84905421739a807704f8/websockets-11.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288", size = 120975 },
{ url = "https://files.pythonhosted.org/packages/8c/a8/e81533499f84ef6cdd95d11d5b05fa827c0f097925afd86f16e6a2631d8e/websockets-11.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d", size = 121017 },
{ url = "https://files.pythonhosted.org/packages/6b/ca/65d6986665888494eca4d5435a9741c822022996f0f4200c57ce4b9242f7/websockets-11.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3", size = 131200 },
{ url = "https://files.pythonhosted.org/packages/c0/a8/a8a582ebeeecc8b5f332997d44c57e241748f8a9856e06a38a5a13b30796/websockets-11.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b", size = 130195 },
{ url = "https://files.pythonhosted.org/packages/a9/5e/b25c60067d700e811dccb4e3c318eeadd3a19d8b3620de9f97434af777a7/websockets-11.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6", size = 130569 },
{ url = "https://files.pythonhosted.org/packages/14/fc/5cbbf439c925e1e184a0392ec477a30cee2fabc0e63807c1d4b6d570fb52/websockets-11.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97", size = 136015 },
{ url = "https://files.pythonhosted.org/packages/0f/d8/a997d3546aef9cc995a1126f7d7ade96c0e16c1a0efb9d2d430aee57c925/websockets-11.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf", size = 135292 },
{ url = "https://files.pythonhosted.org/packages/89/8f/707a05d5725f956c78d252a5fd73b89fa3ac57dd3959381c2d1acb41cb13/websockets-11.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd", size = 135890 },
{ url = "https://files.pythonhosted.org/packages/b5/94/ac47552208583d5dbcce468430c1eb2ae18962f6b3a694a2b7727cc60d4a/websockets-11.0.3-cp311-cp311-win32.whl", hash = "sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c", size = 124149 },
{ url = "https://files.pythonhosted.org/packages/e1/7c/0ad6e7ef0a054d73092f616d20d3d9bd3e1b837554cb20a52d8dd9f5b049/websockets-11.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8", size = 124670 },
{ url = "https://files.pythonhosted.org/packages/47/96/9d5749106ff57629b54360664ae7eb9afd8302fad1680ead385383e33746/websockets-11.0.3-py3-none-any.whl", hash = "sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6", size = 118056 },
]
[[package]]
name = "wrapt"
version = "1.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 },
{ url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 },
{ url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 },
{ url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 },
{ url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 },
{ url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 },
{ url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 },
{ url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 },
{ url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 },
{ url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 },
{ url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 },
{ url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 },
{ url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 },
{ url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 },
{ url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 },
{ url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 },
{ url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 },
{ url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 },
{ url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 },
{ url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 },
{ url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 },
{ url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 },
{ url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 },
{ url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 },
{ url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 },
{ url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 },
{ url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 },
{ url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 },
{ url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 },
{ url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 },
{ url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 },
{ url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 },
{ url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 },
{ url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 },
{ url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 },
{ url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 },
{ url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 },
{ url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 },
{ url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 },
{ url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 },
{ url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 },
{ url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 },
{ url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 },
{ url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 },
{ url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 },
]
[[package]]
name = "yarl"
version = "1.20.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833 },
{ url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070 },
{ url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818 },
{ url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003 },
{ url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537 },
{ url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358 },
{ url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362 },
{ url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979 },
{ url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274 },
{ url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294 },
{ url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169 },
{ url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776 },
{ url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341 },
{ url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988 },
{ url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113 },
{ url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485 },
{ url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686 },
{ url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667 },
{ url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025 },
{ url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709 },
{ url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287 },
{ url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429 },
{ url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429 },
{ url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862 },
{ url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616 },
{ url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954 },
{ url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575 },
{ url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061 },
{ url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142 },
{ url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894 },
{ url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378 },
{ url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069 },
{ url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249 },
{ url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710 },
{ url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811 },
{ url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078 },
{ url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748 },
{ url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595 },
{ url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616 },
{ url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324 },
{ url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676 },
{ url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614 },
{ url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766 },
{ url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615 },
{ url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982 },
{ url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792 },
{ url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049 },
{ url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774 },
{ url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252 },
{ url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198 },
{ url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346 },
{ url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826 },
{ url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217 },
{ url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700 },
{ url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644 },
{ url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452 },
{ url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378 },
{ url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261 },
{ url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987 },
{ url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361 },
{ url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460 },
{ url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486 },
{ url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219 },
{ url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693 },
{ url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803 },
{ url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709 },
{ url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591 },
{ url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003 },
{ url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542 },
]