Compare commits

..

19 Commits

Author SHA1 Message Date
Dogukan Karatas f2efbf57bb Merge pull request #274 from specklesystems/jrm/no-deps
fix(pip): install
2025-06-05 14:50:44 +02:00
Jedd Morgan 74347fe5e0 No deps 2025-06-05 13:47:25 +01:00
Dogukan Karatas a7c6969a4a Merge pull request #273 from specklesystems/dogukan/rename-installer
v2 - renames the installer
2025-06-05 12:50:05 +02:00
Dogukan Karatas 423c145984 renames the installer 2025-06-05 12:46:32 +02:00
Dogukan Karatas c6ecf55f59 Merge pull request #261 from specklesystems/bilal/cnx-1815-blender-connector-legacy-to-next-gen-transition
v2 - add legacy warnings everywhere
2025-06-02 14:43:01 +02:00
Dogukan Karatas c402a388d8 Merge pull request #266 from specklesystems/jrm/bump-deps-v2
Updated dependencies for workspaces fix
2025-05-19 12:14:52 +02:00
Jedd Morgan bc3d4d26d2 Updated dependencies for workspaces fix 2025-05-19 11:10:30 +01:00
Mucahit Bilal GOKER 9397774e24 add a warning to the panel 2025-05-15 13:56:31 +03:00
Mucahit Bilal GOKER ddbcb7ee39 update addon description 2025-05-15 13:56:16 +03:00
Mucahit Bilal GOKER 105bb00304 add legacy to n menu 2025-05-15 13:39:17 +03:00
Jedd Morgan ffd4da1ed9 fix(CI): Fix broken signing (#244)
* Poetry update

* Updated runner

* bump poetry again

* require export

* Fixed Digicerts broken sync certs
2025-04-23 11:35:23 +01:00
Jedd Morgan 26ba5c921b Poetry update (#243)
* Poetry update

* Updated runner

* bump poetry again

* require export
2025-04-22 18:26:01 +01:00
Jedd Morgan a3aaf4471c Bumped Dependencies (#215)
* Updated dependencies

* black + isort
2024-12-11 14:12:41 +00:00
Jedd Morgan 4995f02351 Fixed issue with local server builds (#212) 2024-11-22 11:57:55 +00:00
Jedd Morgan a456cdf38b bump speckle py (#211) 2024-11-18 12:45:43 +00:00
Jedd Morgan 7d9ef2d418 hotfix for previous PR mistakenly calling the get_project_workspace_id function with incorrect args (#210)
* workspace tracking + black

* works

* fixed issue
2024-11-05 17:56:34 +00:00
Jedd Morgan b8b7c0bdf5 Jedd/cxpla 94 track workspace id in specklepy connector metrics (#209)
* workspace tracking + black

* works
2024-11-05 13:09:38 +00:00
Claire Kuang f7c4fc3665 Merge pull request #208 from specklesystems/claire/cnx-699-update-github-links-to-point-to-v3
Update README.md to align with main github page
2024-11-01 18:46:20 +00:00
Claire Kuang cb9ba23c0c Update README.md to align with main github page 2024-11-01 18:45:25 +00:00
68 changed files with 6428 additions and 4026 deletions
+326 -12
View File
@@ -1,17 +1,331 @@
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.12
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.12
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:2025.04
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
environment:
SSM: 'C:\Program Files\DigiCert\DigiCert One Signing Manager Tools'
steps:
- attach_workspace:
at: ./
- run:
name: Patch installer
command: python patch_installer.py (Get-Content -Raw SEMVER)
- unless: # Build installers unsigned on non-tagged builds
condition: << pipeline.git.tag >>
steps:
- run:
name: Build Installer
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p
shell: cmd.exe #does not work in powershell
- when: # Setup certificates and build installers signed for tagged builds
condition: << pipeline.git.tag >>
steps:
- run:
name: "Digicert Signing Manager Setup"
command: |
cd C:\
curl.exe -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -H "x-api-key:$env:SM_API_KEY" -o smtools-windows-x64.msi
msiexec.exe /i smtools-windows-x64.msi /quiet /qn | Wait-Process
- run:
name: Create Auth & OV Signing Cert
command: |
cd C:\
echo $env:SM_CLIENT_CERT_FILE_B64 > certificate.txt
certutil -decode certificate.txt certificate.p12
- run:
name: Sync Certs
command: |
& $env:SSM\smctl.exe windows certsync
- run:
name: Build Installer
command: speckle-sharp-ci-tools\InnoSetup\ISCC.exe speckle-sharp-ci-tools\blender.iss /Sbyparam=$p /DSIGN_INSTALLER /DCODE_SIGNING_CERT_FINGERPRINT=%SM_CODE_SIGNING_CERT_SHA1_HASH%
shell: cmd.exe #does not work in powershell
- persist_to_workspace:
root: ./
paths:
- speckle-sharp-ci-tools/Installers/blender/blender-*.exe
build-installer-mac:
macos:
xcode: 13.4.1
resource_class: macos.m1.medium.gen1
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: Exit if External PR
command: if [ "$CIRCLE_PR_REPONAME" ]; then circleci-agent step halt; fi
- 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:2025.04
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: digicert-keylocker
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
-91
View File
@@ -1,91 +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:
| # Processing the ref manually atm.. likely we'll want to use Adam's fancy logic at some point instead
TAG=${{ github.ref_name }}
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
TAG="3.0.99.${{ github.run_number }}"
fi
SEMVER=$(echo "$TAG" | sed -E 's/\/[a-zA-Z-]+//')
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z]+\.([0-9]+))?/\1.\2.\3.\5/' | sed 's/\.$/.0/')
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_TAG_BUILD: ${{ github.ref_type == 'tag' }}
IS_RELEASE_BRANCH: ${{ startsWith(github.ref_name, 'installer-test/') || github.ref_name == 'main'}}
steps:
- name: 🔫 Trigger Build Installer(s)
uses: ALEEF02/workflow-dispatch@v3.0.0
with:
workflow: Build Blender Installer
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 }}","public_release": ${{ env.IS_TAG_BUILD }}, "store_artifacts": ${{ env.IS_RELEASE_BRANCH }} }'
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
+10 -35
View File
@@ -1,45 +1,20 @@
<h1 align="center">
<<h1 align="center">
<img src="https://user-images.githubusercontent.com/2679513/131189167-18ea5fe1-c578-47f6-9785-3748178e4312.png" width="150px"/><br/>
Speckle | Blender
</h1>
<h3 align="center">
Connector for Blender
</h3>
<p align="center"><b>Speckle</b> is the data infrastructure for the AEC industry.</p><br/>
<p align="center"><a href="https://twitter.com/SpeckleSystems"><img src="https://img.shields.io/twitter/follow/SpeckleSystems?style=social" alt="Twitter Follow"></a> <a href="https://speckle.community"><img src="https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&amp;style=flat-square&amp;logo=discourse&amp;logoColor=white" alt="Community forum users"></a> <a href="https://speckle.systems"><img src="https://img.shields.io/badge/https://-speckle.systems-royalblue?style=flat-square" alt="website"></a> <a href="https://speckle.guide/dev/"><img src="https://img.shields.io/badge/docs-speckle.guide-orange?style=flat-square&amp;logo=read-the-docs&amp;logoColor=white" alt="docs"></a></p>
<p align="center"><a href="https://github.com/specklesystems/speckle-blender/"><img src="https://circleci.com/gh/specklesystems/speckle-blender.svg?style=svg&amp;circle-token=76eabd350ea243575cbb258b746ed3f471f7ac29" alt="Speckle-Next"></a> </p>
# About Speckle
> Speckle is the first AEC data hub that connects with your favorite AEC tools. Speckle exists to overcome the challenges of working in a fragmented industry where communication, creative workflows, and the exchange of data are often hindered by siloed software and processes. It is here to make the industry better.
What is Speckle? Check our ![YouTube Video Views](https://img.shields.io/youtube/views/B9humiSpHzM?label=Speckle%20in%201%20minute%20video&style=social)
### Features
- **Object-based:** say goodbye to files! Speckle is the first object based platform for the AEC industry
- **Version control:** Speckle is the Git & Hub for geometry and BIM data
- **Collaboration:** share your designs collaborate with others
- **3D Viewer:** see your CAD and BIM models online, share and embed them anywhere
- **Interoperability:** get your CAD and BIM models into other software without exporting or importing
- **Real time:** get real time updates and notifications and changes
- **GraphQL API:** get what you need anywhere you want it
- **Webhooks:** the base for a automation and next-gen pipelines
- **Built for developers:** we are building Speckle with developers in mind and got tools for every stack
- **Built for the AEC industry:** Speckle connectors are plugins for the most common software used in the industry such as Revit, Rhino, Grasshopper, AutoCAD, Civil 3D, Excel, Unreal Engine, Unity, QGIS, Blender and more!
### Try Speckle now!
Give Speckle a try in no time by:
- [![speckle XYZ](https://img.shields.io/badge/https://-speckle.xyz-0069ff?style=flat-square&logo=hackthebox&logoColor=white)](https://speckle.xyz) ⇒ creating an account at our public server
- [![create a droplet](https://img.shields.io/badge/Create%20a%20Droplet-0069ff?style=flat-square&logo=digitalocean&logoColor=white)](https://marketplace.digitalocean.com/apps/speckle-server?refcode=947a2b5d7dc1) ⇒ deploying an instance in 1 click
### Resources
- [![Community forum users](https://img.shields.io/badge/community-forum-green?style=for-the-badge&logo=discourse&logoColor=white)](https://speckle.community) for help, feature requests or just to hang with other speckle enthusiasts, check out our community forum!
- [![website](https://img.shields.io/badge/tutorials-speckle.systems-royalblue?style=for-the-badge&logo=youtube)](https://speckle.systems) our tutorials portal is full of resources to get you started using Speckle
- [![docs](https://img.shields.io/badge/docs-speckle.guide-orange?style=for-the-badge&logo=read-the-docs&logoColor=white)](https://speckle.guide/user/blender.html) reference on almost any end-user and developer functionality
<h3 align="center">
Speckle Connector for Blender
</h3>
> [!WARNING]
> This is a legacy repo! A new next generation connector will be coming soon. In the meantime, check out our active next generation repos here 👇<br/>
> [`speckle-sharp-connectors`](https://github.com/specklesystems/speckle-sharp-connectors): our .NET next generation connectors and desktop UI<br/>
> [`speckle-sharp-sdk`](https://github.com/specklesystems/speckle-sharp-sdk): our .NET SDK, Tests, and Objects
# Blender Connector
@@ -83,7 +58,7 @@ The full matrix of supported Blender and Speckle types [can be found here](https
## 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 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 uv, we install the correct version of each dependency for your blender python version, host OS, and system architecture.
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.
Other blender addons may require dependencies that conflict with specklepy. In these cases, one or both addons may fail to load.
+86 -78
View File
@@ -1,106 +1,114 @@
# 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 .connector.ui import icons
import json
# 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 bpy.app.handlers import persistent
from specklepy.logging import metrics
from bpy_speckle.callbacks import *
from bpy_speckle.operators import *
from bpy_speckle.properties import *
from bpy_speckle.ui import *
bl_info = {
"name": "Speckle Blender ",
"name": "SpeckleBlender 2.0 (Legacy)",
"author": "Speckle Systems",
"version": (3, 999, 999),
"blender": (4, 2, 0),
"version": (0, 2, 0),
"blender": (2, 92, 0),
"location": "3d viewport toolbar (N), under the Speckle tab.",
"description": "The Speckle Connector using specklepy 3.x!",
"warning": "This add-on is WIP and should be used with caution",
"description": "(Legacy) Speckle Connector using specklepy 2.0!",
"warning": "This is a legacy add-on. Download the new connector from https://app.speckle.systems/connectors",
"wiki_url": "https://github.com/specklesystems/speckle-blender",
"category": "Scene",
}
# UI
from .connector.ui.main_panel import SPECKLE_PT_main_panel
from .connector.ui.project_selection_dialog import SPECKLE_OT_project_selection_dialog, speckle_project, SPECKLE_UL_projects_list, SPECKLE_OT_add_project_by_url
from .connector.ui.model_selection_dialog import SPECKLE_OT_model_selection_dialog, speckle_model, SPECKLE_UL_models_list
from .connector.ui.version_selection_dialog import SPECKLE_OT_version_selection_dialog, speckle_version, SPECKLE_UL_versions_list
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog
from .connector.ui.model_card import 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
from .connector.blender_operators.select_objects import SPECKLE_OT_select_objects
"""
Import SpeckleBlender classes
"""
# States
from .connector.states.speckle_state import register as register_speckle_state, unregister as unregister_speckle_state
def save_model_cards(scene):
model_cards_data = [card.to_dict() for card in scene.speckle_state.model_cards]
scene["speckle_model_cards_data"] = json.dumps(model_cards_data)
def load_model_cards(scene):
if "speckle_model_cards_data" in scene:
model_cards_data = json.loads(scene["speckle_model_cards_data"])
scene.speckle_state.model_cards.clear()
for card_data in model_cards_data:
card = speckle_model_card.from_dict(card_data)
scene.speckle_state.model_cards.add().update(card)
"""
Add load handler to initialize Speckle when
loading a Blender file
"""
# 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_OT_add_project_by_url,
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_model_card, SPECKLE_OT_model_card_settings, SPECKLE_OT_view_in_browser, SPECKLE_OT_view_model_versions,
SPECKLE_OT_select_objects)
@bpy.app.handlers.persistent
@persistent
def load_handler(dummy):
load_model_cards(bpy.context.scene)
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)
@bpy.app.handlers.persistent
def save_handler(dummy):
save_model_cards(bpy.context.scene)
# 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)
metrics.set_host_app("blender", f"blender {bpy.app.version_string}")
"""
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)
bpy.app.handlers.save_post.append(save_handler)
def unregister():
icons.unload_icons()
unregister_speckle_state() # Unregister SpeckleState
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(load_handler)
bpy.app.handlers.save_post.remove(save_handler)
# Run the register function when the script is executed
"""
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)
if __name__ == "__main__":
register()
@@ -0,0 +1,140 @@
from typing import Dict, Optional, Union
import bpy
from attrs import define
from bpy.types import ID, Collection, Object
from specklepy.objects.base import Base
from specklepy.objects.graph_traversal.commit_object_builder import (
ROOT,
CommitObjectBuilder,
)
from specklepy.objects.other import Collection as SCollection
from bpy_speckle.functions import _report
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
-74
View File
@@ -1,74 +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 for Blender BETA"
tagline = "Speckle connector for Blender"
maintainer = "AEC SYSTEMS LTD"
# Supported types: "add-on", "theme"
type = "add-on"
# Optional link to documentation, support, source files, etc
website = "https://speckle.guide/user/blender.html"
# 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 .draw_speckle_info import draw_speckle_info
from .on_mesh_edit import scb_on_mesh_edit
@@ -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,3 +0,0 @@
from ..blender_operators.load_button import SPECKLE_OT_load # 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
@@ -1,27 +0,0 @@
import bpy
from typing import Set
from bpy.types import Context
class SPECKLE_OT_load(bpy.types.Operator):
bl_idname = "speckle.load"
bl_label = "Load from Speckle"
bl_description = "Load objects from Speckle"
def invoke(self, context: Context, event: bpy.types.Event) -> Set[str]:
# Captures cursor position for UI placement
context.scene.speckle_state.mouse_position = (event.mouse_x, event.mouse_y)
return self.execute(context)
def execute(self, context: Context) -> Set[str]:
# Sets the UI mode to LOAD
context.scene.speckle_state.ui_mode = "LOAD"
# Logs cursor position
self.report(
{"INFO"},
f"Load button clicked at {context.scene.speckle_state.mouse_position[0], context.scene.speckle_state.mouse_position[1]}",
)
# Opens project_selection_dialog
bpy.ops.speckle.project_selection_dialog("INVOKE_DEFAULT")
return {"FINISHED"}
@@ -1,72 +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.
Attributes:
model_name (StringProperty): Name of the model being configured.
"""
bl_idname = "speckle.model_card_settings"
bl_label = "Model Card Settings"
bl_description = "Settings for the model card"
model_card_index: bpy.props.IntProperty(name="Model Card Index", default=0) # type: ignore
def execute(self, context: Context) -> Set[str]:
self.report({'INFO'}, f"Settings for {context.scene.speckle_state.model_cards[self.model_card_index]}")
return {'FINISHED'}
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
# Add a button for viewing 3d model in the browser
layout.operator("speckle.view_in_browser", text="View in Browser").model_card_index = self.model_card_index
# Add a button for viewing model versions in the browser
layout.operator("speckle.view_model_versions", text="View Model Versions").model_card_index = self.model_card_index
def invoke(self, context: Context, event: Event) -> Set[str]:
wm = context.window_manager
return wm.invoke_props_dialog(self)
# Operator for viewing the model in the browser
class SPECKLE_OT_view_in_browser(bpy.types.Operator):
"""Opens the current model in the Speckle web viewer.
This operator opens the default web browser to display the current
model in the Speckle web app.
"""
bl_idname = "speckle.view_in_browser"
bl_label = "View in Browser"
bl_description = "View the model in the browser"
model_card_index: bpy.props.IntProperty() #type: ignore
def execute(self, context: Context) -> Set[str]:
model_card = context.scene.speckle_state.model_cards[self.model_card_index]
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'}
# Operator for viewing the model versions in the browser
class SPECKLE_OT_view_model_versions(bpy.types.Operator):
"""Opens the model's version history in the Speckle web app.
This operator opens the default web browser to display the version
history of the current model 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_index: bpy.props.IntProperty() #type: ignore
def execute(self, context: Context) -> Set[str]:
model_card = context.scene.speckle_state.model_cards[self.model_card_index]
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'}
@@ -1,25 +0,0 @@
import bpy
from bpy.types import Context
from bpy.types import Event
from typing import Set
# Publish Operator
class SPECKLE_OT_publish(bpy.types.Operator):
bl_idname = "speckle.publish"
bl_label = "Publish to Speckle"
bl_description = "Publish selected objects to Speckle"
def invoke(self, context: Context, event: Event) -> Set[str]:
# Captures the mouse position for UI placement
context.scene.speckle_state.mouse_position = (event.mouse_x, event.mouse_y)
return self.execute(context)
def execute(self, context: Context) -> Set[str]:
# Sets UI mode to PUBLISH
context.scene.speckle_state.ui_mode = "PUBLISH"
# Logs click position
self.report({'INFO'}, f"Publish button clicked at {context.scene.speckle_state.mouse_position[0], context.scene.speckle_state.mouse_position[1]}")
# Opens project selection dialog
bpy.ops.speckle.project_selection_dialog("INVOKE_DEFAULT")
return {'FINISHED'}
@@ -1,50 +0,0 @@
import bpy
from bpy.types import Operator
from bpy.props import IntProperty
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'}
model_card_index: IntProperty(
name="Model Card Index",
description="Index of the model card",
default=0
)
def execute(self, context):
# Get the model card
model_card = context.scene.speckle_state.model_cards[self.model_card_index]
# Construct collection name
collection_name = f"{model_card.model_name} - {model_card.version_id[:8]}"
# Find the collection
collection = bpy.data.collections.get(collection_name)
if not collection:
self.report({'ERROR'}, f"Collection {collection_name} not found")
return {'CANCELLED'}
# Deselect all objects first
bpy.ops.object.select_all(action='DESELECT')
# Select all objects in the collection and its child collections
def select_collection_objects(collection):
for obj in collection.objects:
obj.select_set(True)
for child in collection.children:
select_collection_objects(child)
select_collection_objects(collection)
# Set active object to first selected object if any objects were selected
selected = context.selected_objects
if selected:
context.view_layer.objects.active = selected[0]
# Frame selected objects in the viewport
bpy.ops.view3d.view_selected()
self.report({'INFO'}, f"Selected {len(context.selected_objects)} objects")
return {'FINISHED'}
@@ -1,2 +0,0 @@
from ..blender_operators.load_button import SPECKLE_OT_load # noqa: F401
from ..operations.load_operation import load_operation # noqa: F401
@@ -1,263 +0,0 @@
import bpy
from bpy.types import Context
from specklepy.api.credentials import get_local_accounts
from specklepy.transports.server import ServerTransport
from specklepy.api import operations
from specklepy.api.client import SpeckleClient
from specklepy.objects.models.collections.collection import Collection as SCollection
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from ..utils.get_ascendants import get_ascendants
from ...converter.to_native import convert_to_native, render_material_proxy_to_native
def load_operation(context: Context, model_card) -> None:
"""
load objects from Speckle and maintain hierarchy.
"""
# get account
# to discuss: this looks redundant, we need to cache it somehow
account = next(
(
acc
for acc in get_local_accounts()
if acc.id == context.window_manager.selected_account_id
),
None,
)
if account is None:
print("No Speckle account found")
return
# initialize the Speckle client
client = SpeckleClient(host=account.serverInfo.url)
# authenticate with account
client.authenticate_with_account(account)
# create a transport
transport = ServerTransport(stream_id=model_card.project_id, client=client)
# get the version
version = client.version.get(model_card.version_id, model_card.project_id)
obj_id = version.referenced_object
# receive the data
version_data = operations.receive(obj_id, transport)
# process materials from the root object
material_mapping = render_material_proxy_to_native(version_data)
print(f"Created material mapping for {len(material_mapping)} objects")
# default traversal function
traversal_function = create_default_traversal_function()
# create a root collection in Blender to hold all imported objects
root_collection_name = f"{model_card.model_name} - {model_card.version_id[:8]}"
root_collection = bpy.data.collections.new(root_collection_name)
context.scene.collection.children.link(root_collection)
# start conversion process
context.window_manager.progress_begin(0, 100)
# dictionary to track converted objects by Speckle ID
converted_objects = {}
# dictionary to track created collections by name to avoid duplicates
created_collections = {}
created_collections[root_collection_name] = root_collection
print("Creating collection hierarchy...")
# first create a complete map of the Speckle hierarchy
collection_hierarchy = {}
all_objects = {}
# track the root collection ID from Speckle
speckle_root_id = None
for traversal_item in traversal_function.traverse(version_data):
speckle_obj = traversal_item.current
if not hasattr(speckle_obj, "id"):
continue
# store all objects for later reference
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):
# track the top-level collection (the one with no parents)
if not parent_ascendants and speckle_root_id is None:
speckle_root_id = speckle_obj.id
# get collection name
collection_name = getattr(
speckle_obj, "name", f"Collection_{speckle_obj.id[:8]}"
)
# find immediate parent collection if any
parent_id = None
for parent in parent_ascendants:
if isinstance(parent, SCollection) and hasattr(parent, "id"):
parent_id = parent.id
break
# store collection info
collection_hierarchy[speckle_obj.id] = {
"id": speckle_obj.id,
"name": collection_name,
"parent_id": parent_id,
"blender_collection": None,
"full_path": [collection_name], # start the path with this collection
}
# build full path hierarchy
if parent_id in collection_hierarchy:
collection_hierarchy[speckle_obj.id]["full_path"] = (
collection_hierarchy[parent_id]["full_path"] + [collection_name]
)
else:
# for non-collection objects, just store their parent information
if hasattr(speckle_obj, "id"):
# find immediate parent collection
parent_id = None
for parent in parent_ascendants:
if isinstance(parent, SCollection) and hasattr(parent, "id"):
parent_id = parent.id
break
# create all collections in the right order
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)
# sort collections by depth to ensure parents are created before children
sorted_collections = sorted(
collection_hierarchy.keys(),
key=lambda coll_id: (
get_collection_depth(coll_id),
collection_hierarchy[coll_id]["name"],
),
)
# map the Speckle root collection to our Blender root collection
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:
# skip the root collection (already handled)
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"]
# key to use for checking if collection already exists
collection_key = tuple(full_path)
# determine parent collection
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"]
# create or find the collection
if collection_key in created_collections:
blender_collection = created_collections[collection_key]
else:
blender_collection = bpy.data.collections.new(coll_name)
parent_collection.children.link(blender_collection)
created_collections[collection_key] = blender_collection
# store the created 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
# skip collections (already handled)
if isinstance(speckle_obj, SCollection):
continue
# skip if already processed
if hasattr(speckle_obj, "id") and speckle_obj.id in converted_objects:
continue
try:
# convert here, passing the material mapping
blender_obj = convert_to_native(speckle_obj, material_mapping)
if blender_obj is None:
print(f"No converter found for: {speckle_obj.speckle_type}")
continue
# store the converted object
if hasattr(speckle_obj, "id"):
converted_objects[speckle_obj.id] = blender_obj
# determine which collection this object should be placed in
target_collection = root_collection
ascendants = list(get_ascendants(traversal_item))
# find immediate parent collection by walking up the hierarchy
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
# link object to the target collection
try:
# check if already linked
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()
# update progress
conversion_count += 1
if conversion_count % 10 == 0:
context.window_manager.progress_update(min(conversion_count, 100))
# end progress bar
context.window_manager.progress_end()
# select the new collection in the outliner
for area in context.screen.areas:
if area.type == "OUTLINER":
area.tag_redraw()
print(f"Load process completed. Imported {len(converted_objects)} objects.")
@@ -1,30 +0,0 @@
import bpy
from bpy.props import CollectionProperty, StringProperty, IntProperty, IntVectorProperty
from bpy.types import PropertyGroup
from ..ui.model_card import speckle_model_card
class SpeckleState(PropertyGroup):
"""Manages the state of the Speckle addon in Blender.
This class stores UI-related state information for the Speckle addon, including
the current UI mode, model cards collection, and UI interaction states.
Attributes:
ui_mode (StringProperty): Current UI mode of the addon. Defaults to "NONE".
model_cards (CollectionProperty): Collection of Speckle model cards.
model_card_index (IntProperty): Index of the currently selected model card.
mouse_position (IntVectorProperty): 2D vector storing current mouse position.
"""
ui_mode: StringProperty(name="UI Mode", default="NONE") # type: ignore
model_cards: CollectionProperty(type=speckle_model_card) # type: ignore
model_card_index: IntProperty(name="Model Card Index", default=0) # type: ignore
mouse_position: IntVectorProperty(size=2) # type: ignore
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
-22
View File
@@ -1,22 +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_icons.load("speckle_logo", os.path.join(icons_dir, "speckle-logo.png"), 'IMAGE')
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
-88
View File
@@ -1,88 +0,0 @@
"""Module for handling the main Speckle panel.
"""
import bpy
from bpy.types import UILayout, Context
from .icons import get_icon
# Main Panel
class SPECKLE_PT_main_panel(bpy.types.Panel):
"""Main panel for the Speckle addon.
This panel serves as the primary interface for the Speckle addon:
- Buttons for publishing and loading models
- Model cards showing the status of each Speckle model in the file
- Quick access to model settings and operations
The panel is displayed in the 3D View's sidebar under the 'Speckle' category.
"""
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 BETA", 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.")
# Add some space
layout.separator()
# Publish and Load buttons
row: UILayout = layout.row()
# row.operator("speckle.publish", text="Publish", icon='EXPORT')
row.operator("speckle.load", text="Load", icon='IMPORT')
layout.separator()
# Group model cards by project name
project_groups = {}
for model_card_index, model_card in enumerate(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_index, model_card))
# Render model cards grouped by project
for project_name, model_cards in project_groups.items():
# Create a collapsable group for each project
project_box = layout.box()
project_row = project_box.row()
project_row.label(text=f"Project: {project_name}", icon='TRIA_RIGHT')
# Render model cards for this project
for model_card_index, model_card in model_cards:
box: UILayout = project_box.box()
row: UILayout = box.row()
icon: str = 'EXPORT' if model_card.is_publish else 'IMPORT'
row.operator("speckle.publish", text="", icon=icon)
row.label(text=f"{model_card.model_name}")
# Add selection button
select_op = row.operator("speckle.select_objects", text="", icon='RESTRICT_SELECT_OFF')
select_op.model_card_index = model_card_index
row.operator("speckle.model_card_settings", text="", icon='PREFERENCES').model_card_index = model_card_index
row: UILayout = box.row()
# Display selection summary or version ID
if model_card.is_publish:
# This adjusts the layout of the row (button 1/3, label 2/3 )
split: UILayout = row.split(factor=0.33)
# TODO: Connect to selection operator
split.operator("speckle.publish", text="Selection")
split.label(text=f"{model_card.selection_summary}")
else:
# This adjusts the layout of the row (button 1/3, label 2/3 )
split: UILayout = row.split(factor=0.33)
# TODO: Connect to version operator
if model_card.load_option == "LATEST":
split.operator("speckle.load", text="Latest")
if model_card.load_option == "SPECIFIC":
split.operator("speckle.load", text=f"{model_card.version_id}")
# TODO: Get last updated time
split.label(text="Last updated: 2 days ago")
-63
View File
@@ -1,63 +0,0 @@
import bpy
from typing import Dict, Any
class speckle_model_card(bpy.types.PropertyGroup):
"""Represents a Speckle model card in the Blender UI.
This class stores information about a Speckle model, including its project name,
whether if its publish or load, and version information. It is used to display and manage model
cards in the Blender interface.
Attributes:
project_name (StringProperty): Name of the project containing the model.
model_name (StringProperty): Name of the Speckle model.
is_publish (BoolProperty): Flag indicating if the model is being published (True) or loaded (False).
selection_summary (StringProperty): Summary text of the current object selection.
version_id (StringProperty): Unique identifier of the selected version.
"""
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="Version ID", description="ID of the selected version", default="") # type: ignore
def to_dict(self) -> Dict[str, Any]:
"""Converts the model card to a dictionary representation.
Returns:
dict: A dictionary containing all model card properties with their current values.
"""
return {
"server_url": self.server_url,
"project_name": self.project_name,
"project_id": self.project_id,
"model_id": self.model_id,
"model_name": self.model_name,
"is_publish": self.is_publish,
"selection_summary": self.selection_summary,
"version_id": self.version_id,
}
@classmethod
def from_dict(cls, data):
"""Creates a new model card instance from a dictionary.
Args:
data (dict): Dictionary containing model card properties and their values.
Returns:
speckle_model_card: A new instance of the model card with properties set from the dictionary.
"""
item = cls()
item.server_url = data["server_url"]
item.project_name = data["project_name"]
item.project_id = data["project_id"]
item.model_id = data["model_id"]
item.model_name = data["model_name"]
item.is_publish = data["is_publish"]
item.selection_summary = data["selection_summary"]
item.version_id = data["version_id"]
@@ -1,179 +0,0 @@
"""Module for handling model selection dialog in the Speckle Blender addon.
This module provides the UI components and functionality for selecting models
from Speckle projects within Blender.
"""
import bpy
from bpy.types import UILayout, Context, PropertyGroup, Event, WindowManager
from .mouse_position_mixin import MousePositionMixin
from ..utils.model_manager import get_models_for_project
class speckle_model(bpy.types.PropertyGroup):
"""PropertyGroup for storing model information.
This class stores information about a Speckle model including its name,
ID, and last update time for display in the model selection dialog.
Attributes:
name: The display name of the model.
id: The unique identifier of the model.
updated: The last update timestamp of the model.
"""
# Blender properties use dynamic typing, so we need to ignore type checking
name: bpy.props.StringProperty() # type: ignore
id: bpy.props.StringProperty(name="ID") # type: ignore
updated: bpy.props.StringProperty(name="Updated") # type: ignore
class SPECKLE_UL_models_list(bpy.types.UIList):
"""UIList for displaying a list of Speckle models.
This class handles the visual representation of models in the model selection dialog.
It displays model information in both default/compact and grid layouts.
"""
def draw_item(self, context: Context, layout: UILayout, data: PropertyGroup, item: PropertyGroup,
icon: str, active_data: PropertyGroup, active_propname: str) -> None:
"""Draws a single item in the model list.
Args:
context: The current Blender context.
layout: The layout to draw the item in.
data: The data containing the item.
item: The item to draw.
icon: The icon to use for the item.
active_data: The data containing the active item.
active_propname: The name of the active property.
"""
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)
# This handles when the list is in a grid layout
elif self.layout_type == 'GRID':
layout.alignment = 'CENTER'
layout.label(text=item.name)
class SPECKLE_OT_model_selection_dialog(MousePositionMixin, bpy.types.Operator):
"""Operator for displaying and handling the model selection dialog.
This operator manages the UI and functionality for selecting Speckle models,
including search capabilities and model list display.
Attributes:
search_query: The current search string for filtering models.
project_name: The name of the currently selected project.
project_id: The ID of the currently selected project.
model_index: The index of the currently selected model.
"""
bl_idname = "speckle.model_selection_dialog"
bl_label = "Select Model"
def update_models_list(self, context: Context) -> None:
"""Updates the list of models based on the current project and search query.
Args:
context: The current Blender context.
"""
wm = context.window_manager
# Clear existing models
wm.speckle_models.clear()
# Get models for the selected project, using search if provided
search = self.search_query if self.search_query.strip() else None
models = get_models_for_project(wm.selected_account_id, self.project_id, search=search)
# Populate models list
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
)
project_name: bpy.props.StringProperty( # type: ignore
name="Project Name",
description="The name of the project to select",
default=""
)
project_id: bpy.props.StringProperty( # type: ignore
name="Project ID",
description="The ID of the project to select",
default=""
)
model_index: bpy.props.IntProperty(name="Model Index", default=0) # type: ignore
def execute(self, context: Context) -> set[str]:
selected_model = context.window_manager.speckle_models[self.model_index]
if context.scene.speckle_state.ui_mode == "PUBLISH":
bpy.ops.speckle.selection_filter_dialog("INVOKE_DEFAULT",
project_name=self.project_name,
project_id=self.project_id,
model_name=selected_model.name,
model_id=selected_model.id)
elif context.scene.speckle_state.ui_mode == "LOAD":
bpy.ops.speckle.version_selection_dialog("INVOKE_DEFAULT",
project_name=self.project_name,
project_id=self.project_id,
model_name=selected_model.name,
model_id=selected_model.id)
return {'FINISHED'}
def invoke(self, context: Context, event: Event) -> set[str]:
# Ensure WindowManager has the projects collection
if not hasattr(WindowManager, "speckle_models"):
# Register the collection property
WindowManager.speckle_models = bpy.props.CollectionProperty(type=speckle_model)
# Update models list
self.update_models_list(context)
# Store the original mouse position
self.init_mouse_position(context, event)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout : UILayout = self.layout
layout.label(text=f"Project: {self.project_name}")
# Search field
row = layout.row(align=True)
row.prop(self, "search_query", icon='VIEWZOOM', text="")
# Models UIList
layout.template_list("SPECKLE_UL_models_list", "", context.window_manager, "speckle_models", self, "model_index")
layout.separator()
# Move cursor to original position
self.restore_mouse_position(context)
def register() -> None:
bpy.utils.register_class(speckle_model)
bpy.utils.register_class(SPECKLE_UL_models_list)
bpy.utils.register_class(SPECKLE_OT_model_selection_dialog)
def unregister() -> None:
# Clean up WindowManager properties
if hasattr(WindowManager, "speckle_models"):
del WindowManager.speckle_models
bpy.utils.unregister_class(SPECKLE_OT_model_selection_dialog)
bpy.utils.unregister_class(SPECKLE_UL_models_list)
bpy.utils.unregister_class(speckle_model)
@@ -1,16 +0,0 @@
import bpy
from bpy.types import Context, Event
class MousePositionMixin:
original_mouse_position: bpy.props.IntVectorProperty(size=2) # type: ignore
mouse_snap: bpy.props.BoolProperty(name="Mouse Snap", default=False) # type: ignore
def init_mouse_position(self, context: Context, event: Event) -> None:
self.original_mouse_position = (event.mouse_x, event.mouse_y)
self.mouse_snap = False
context.window.cursor_warp(context.scene.speckle_state.mouse_position[0], context.scene.speckle_state.mouse_position[1])
def restore_mouse_position(self, context: Context) -> None:
if not self.mouse_snap:
self.mouse_snap = True
context.window.cursor_warp(self.original_mouse_position[0], self.original_mouse_position[1])
@@ -1,225 +0,0 @@
"""Module for handling project selection dialog in the Speckle Blender addon.
This module provides the UI components and functionality for selecting projects
from Speckle accounts within Blender.
"""
import bpy
from bpy.types import UILayout, Context, PropertyGroup, Event, WindowManager
from typing import List, Tuple
from ..utils.account_manager import get_account_enum_items, get_default_account_id
from ..utils.project_manager import get_projects_for_account
class speckle_project(bpy.types.PropertyGroup):
"""PropertyGroup for storing project information.
This class stores information about a Speckle project including its name,
role, update time, and ID for display in the project selection dialog.
Attributes:
name: The display name of the project.
role: User's role in the project.
updated: The last update timestamp of the project.
id: The unique identifier of the project.
"""
# Blender properties use dynamic typing, so we need to ignore type checking
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
class SPECKLE_UL_projects_list(bpy.types.UIList):
"""UIList for displaying a list of Speckle projects.
This class handles the visual representation of projects in the project selection dialog.
It displays project information in both default/compact and grid layouts.
"""
def draw_item(self, context: Context, layout: UILayout, data: PropertyGroup, item: PropertyGroup, icon: str, active_data: PropertyGroup, active_propname: str) -> None:
"""Draws a single item in the project list.
Args:
context: The current Blender context.
layout: The layout to draw the item in.
data: The data containing the item.
item: The item to draw.
icon: The icon to use for the item.
active_data: The data containing the active item.
active_propname: The name of the active property.
"""
if self.layout_type in {'DEFAULT', 'COMPACT'}:
row = layout.row(align=True)
split = row.split(factor=0.5) # This gives project name 1/2
split.label(text=item.name)
right_split = split.split(factor=0.5) # This gives project role and updated the other 1/2 of the row
right_split.label(text=item.role)
right_split.label(text=item.updated)
# This handles when the list is in a grid layout
elif self.layout_type == 'GRID':
layout.alignment = 'CENTER'
layout.label(text=item.name)
class SPECKLE_OT_project_selection_dialog(bpy.types.Operator):
"""Operator for displaying and handling the project selection dialog.
This operator manages the UI and functionality for selecting Speckle projects,
including account selection and project list display.
Attributes:
search_query: The current search string for filtering projects.
accounts: Available Speckle accounts for selection.
project_index: The index of the currently selected project.
"""
bl_idname = "speckle.project_selection_dialog"
bl_label = "Select Project"
def update_projects_list(self, context: Context) -> None:
"""Updates the list of projects based on the selected account and search query.
Args:
context: The current Blender context.
"""
wm = context.window_manager
# Update the selected account ID in the window manager
wm.selected_account_id = self.accounts
# Clear existing projects
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]] = get_projects_for_account(self.accounts, search=search)
# Populate projects list in WindowManager
for name, role, updated, id in projects:
project: speckle_project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
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
)
accounts: bpy.props.EnumProperty( # type: ignore
name="Account",
description="Selected account to filter projects by",
items=get_account_enum_items(),
default=get_default_account_id(),
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]
bpy.ops.speckle.model_selection_dialog("INVOKE_DEFAULT", project_name=selected_project.name, project_id=selected_project.id)
return {'FINISHED'}
def invoke(self, context: Context, event: Event) -> set[str]:
wm = context.window_manager
# Ensure WindowManager has the projects collection
if not hasattr(WindowManager, "speckle_projects"):
# Register the collection property
WindowManager.speckle_projects = bpy.props.CollectionProperty(type=speckle_project)
# Clear existing projects
wm.speckle_projects.clear()
# Get the selected account
selected_account_id = self.accounts
if not hasattr(WindowManager, "selected_account_id"):
# Register the collection property
WindowManager.selected_account_id = bpy.props.StringProperty()
wm.selected_account_id = selected_account_id
# Fetch projects from server
projects: List[Tuple[str, str, str, str]] = get_projects_for_account(selected_account_id)
# Populate projects list in WindowManager
for name, role, updated, id in projects:
project: speckle_project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
# Account selection
layout.prop(self, "accounts", text="")
# Search field
row = layout.row(align=True)
row.prop(self, "search_query", icon='VIEWZOOM', text="")
# TODO: Add a button for adding a project by URL
#row.operator("speckle.add_project_by_url", icon='URL', text="")
# Projects UIList - now using WindowManager collection
layout.template_list(
"SPECKLE_UL_projects_list", "",
context.window_manager, "speckle_projects",
self, "project_index"
)
layout.separator()
class SPECKLE_OT_add_project_by_url(bpy.types.Operator):
"""Operator for adding a Speckle project by URL.
This operator allows users to add a Speckle project by providing its URL.
Attributes:
url: The URL of the Speckle project to add.
"""
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]:
# TODO: Implement logic to add project using the URL
self.report({'INFO'}, f"Adding project from URL: {self.url}")
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")
def register() -> None:
bpy.utils.register_class(speckle_project)
bpy.utils.register_class(SPECKLE_UL_projects_list)
bpy.utils.register_class(SPECKLE_OT_project_selection_dialog)
bpy.utils.register_class(SPECKLE_OT_add_project_by_url)
def unregister() -> None:
# Clean up WindowManager properties
if hasattr(WindowManager, "speckle_projects"):
del WindowManager.speckle_projects
bpy.utils.unregister_class(SPECKLE_OT_add_project_by_url)
bpy.utils.unregister_class(SPECKLE_OT_project_selection_dialog)
bpy.utils.unregister_class(SPECKLE_UL_projects_list)
bpy.utils.unregister_class(speckle_project)
@@ -1,149 +0,0 @@
"""Module for handling object selection filtering.
This module provides the UI components and functionality for filtering and selecting
Blender objects for publishing to Speckle.
"""
import bpy
from typing import List
from .mouse_position_mixin import MousePositionMixin
from bpy.types import Operator, Context, Object
from bpy.props import EnumProperty, StringProperty
class SPECKLE_OT_selection_filter_dialog(MousePositionMixin, Operator):
"""Operator for handling object selection and filtering.
This operator manages the UI and functionality for selecting and filtering
Blender objects before publishing to Speckle, including selection type options
and selection summary display.
Attributes:
selection_type: The type of selection method to use.
project_name: The name of the selected project.
project_id: The ID of the selected project.
model_name: The name of the selected model.
model_id: The ID of the selected model.
"""
bl_idname = "speckle.selection_filter_dialog"
bl_label = "Select Objects"
selection_type: EnumProperty(
name="Selection",
items=[
("SELECTION", "Selection", "Select objects manually"),
],
default="SELECTION"
) # type: ignore
project_name: StringProperty(
name="Project Name",
description="Name of the selected project",
default=""
) # type: ignore
project_id: StringProperty(
name="Project ID",
description="ID of the selected project",
default=""
) # type: ignore
model_name: StringProperty(
name="Model Name",
description="Name of the selected model",
default=""
) # type: ignore
model_id: StringProperty(
name="Model ID",
description="ID of the selected model",
default=""
) # type: ignore
def execute(self, context: Context) -> set:
model_card = context.scene.speckle_state.model_cards.add()
model_card.project_name = self.project_name
model_card.model_name = self.model_name
model_card.model_id = self.model_id
model_card.project_id = self.project_id
model_card.is_publish = True
# Create the selection summary
selected_objects: list[Object] = context.selected_objects
total_selected: int = len(selected_objects)
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
summary: str = f"{total_selected} objects - "
for obj_type, count in object_types.items():
summary += f"{obj_type}: {count}, "
model_card.selection_summary = summary.strip()
return {'FINISHED'}
def invoke(self, context: Context, event: bpy.types.Event) -> set:
# Initialize mouse position
self.init_mouse_position(context, event)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context):
layout = self.layout
layout.label(text=f"Project: {self.project_name}")
layout.label(text=f"Model: {self.model_name}")
# Selection dropdown
layout.prop(self, "selection_type")
layout.separator()
# Get selected objects
selected_objects: List[Object] = context.selected_objects
total_selected: int = len(selected_objects)
# Create a box for the selection summary
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')
# Display object types and counts
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()
# Restore mouse position
self.restore_mouse_position(context)
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: 445 B

@@ -1,227 +0,0 @@
"""Module for handling version selection dialog in the Speckle Blender addon.
Provides the UI components and functionality for selecting versions.
"""
import bpy
from bpy.types import WindowManager, UILayout, Context, PropertyGroup, Event
from .mouse_position_mixin import MousePositionMixin
from ..utils.version_manager import get_versions_for_model, get_latest_version
from ..utils.account_manager import get_server_url_by_account_id
from ..operations.load_operation import load_operation
class speckle_version(bpy.types.PropertyGroup):
"""PropertyGroup for storing version information.
This class stores information about a Speckle version including its ID,
version message, update time, and source application for display in the
version selection dialog.
Attributes:
id: The unique identifier of the version.
message: The version message associated with the version.
updated: The last update timestamp of the version.
source_app: The application that created this version.
"""
# Blender properties use dynamic typing, so we need to ignore type checking
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_UL_versions_list(bpy.types.UIList):
"""UIList for displaying a list of Speckle versions.
It displays version information in both default/compact and grid layouts.
"""
# 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:
"""Draws a single item in the version list.
Args:
context: The current Blender context.
layout: The layout to draw the item in.
data: The data containing the item.
item: The item to draw.
icon: The icon to use for the item.
active_data: The data containing the active item.
active_propname: The name of the active property.
"""
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)
# This handles when the list is in a grid layout
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.label(text=item.id)
class SPECKLE_OT_version_selection_dialog(MousePositionMixin, bpy.types.Operator):
"""Operator for displaying and handling the version selection dialog.
This operator manages the UI and functionality for selecting Speckle versions,
including version list display and search capabilities.
Attributes:
search_query: The current search string for filtering versions.
project_name: The name of the selected project.
model_name: The name of the selected model.
project_id: The ID of the selected project.
model_id: The ID of the selected model.
version_index: The index of the currently selected version.
"""
bl_idname = "speckle.version_selection_dialog"
bl_label = "Select Version"
search_query: bpy.props.StringProperty( # type: ignore
name="Search", description="Search a project", default=""
)
project_name: bpy.props.StringProperty( # type: ignore
name="Project Name", description="Name of the selected project", default=""
)
model_name: bpy.props.StringProperty( # type: ignore
name="Model Name", description="Name of the selected model", default=""
)
project_id: bpy.props.StringProperty( # type: ignore
name="Project ID", description="ID of the selected project", default=""
)
model_id: bpy.props.StringProperty( # type: ignore
name="Model ID", description="ID of the selected model", default=""
)
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",
)
def update_versions_list(self, context: Context) -> None:
wm = context.window_manager
# Clear existing versions
wm.speckle_versions.clear()
# Get versions for the selected model
search = self.search_query if self.search_query.strip() else None
versions = get_versions_for_model(
account_id=wm.selected_account_id,
project_id=self.project_id,
model_id=self.model_id,
search=search,
)
# Populate versions list
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
model_card = context.scene.speckle_state.model_cards.add()
model_card.server_url = get_server_url_by_account_id(
account_id=wm.selected_account_id
)
model_card.project_name = self.project_name
model_card.project_id = self.project_id
model_card.model_id = self.model_id
model_card.model_name = self.model_name
model_card.is_publish = False
model_card.load_option = self.load_option
# Store the selected version ID
if self.load_option == "LATEST":
version_id, _, _ = get_latest_version(
account_id=context.window_manager.selected_account_id,
project_id=self.project_id,
model_id=self.model_id,
)
model_card.version_id = version_id
else: # SPECIFIC
selected_version = context.window_manager.speckle_versions[
self.version_index
]
model_card.version_id = selected_version.id
# Call the load process class method
load_operation(context=context, model_card=model_card)
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
# Ensure WindowManager has the versions collection
if not hasattr(WindowManager, "speckle_versions"):
# Register the collection property
WindowManager.speckle_versions = bpy.props.CollectionProperty(
type=speckle_version
)
# Update versions list
self.update_versions_list(context)
# Initialize mouse position
self.init_mouse_position(context, event)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.label(text=f"Project: {self.project_name}")
layout.label(text=f"Model: {self.model_name}")
# Radio buttons for load options
layout.prop(self, "load_option", expand=True)
# Show search field and version list only if "Load a specific version" is selected
if self.load_option == "SPECIFIC":
# Search field
row = layout.row(align=True)
row.prop(self, "search_query", icon="VIEWZOOM", text="")
# Versions UIList
layout.template_list(
"SPECKLE_UL_versions_list",
"",
context.window_manager,
"speckle_versions",
self,
"version_index",
)
layout.separator()
# Restore mouse position
self.restore_mouse_position(context)
@@ -1,53 +0,0 @@
from specklepy.api.credentials import get_local_accounts
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
def get_account_enum_items() -> List[Tuple[str, str, str]]:
"""Retrieves a list of Speckle accounts formatted for Blender enum properties.
This function fetches all local Speckle accounts and formats them for use in Blender's
UI dropdown menus. If no accounts are found, it returns a single entry indicating that
no accounts are available.
Returns:
List[Tuple[str, str, str]]: A list of tuples where each tuple contains:
- identifier (str): The account ID or "NO_ACCOUNTS" if none found
- name (str): Display string with format "username - server - email" or error message
- description (str): Empty string, reserved for future use
Note:
If no accounts are found, returns a single tuple with instructions for adding an account.
"""
accounts: List[Account] = get_local_accounts()
if not accounts:
return [("NO_ACCOUNTS", "No accounts found! Please add an account from Manager for Speckle.", "")]
return [(acc.id, f"{acc.userInfo.name} - {acc.serverInfo.url} - {acc.userInfo.email}", "") for acc in accounts]
def get_default_account_id() -> Optional[str]:
"""Retrieves the ID of the default Speckle account.
This function searches through all local Speckle accounts and returns the ID
of the account marked as default.
Returns:
Optional[str]: The ID of the default account if one exists, None otherwise.
"""
return next((acc.id for acc in get_local_accounts() if acc.isDefault), None)
def get_server_url_by_account_id(account_id: str) -> Optional[str]:
"""Retrieves the server URL for a given account ID.
Args:
account_id (str): The ID of the account.
Returns:
Optional[str]: The server URL if the account is found, otherwise None.
"""
accounts: List[Account] = get_local_accounts()
for acc in accounts:
if acc.id == account_id:
return acc.serverInfo.url
return None
@@ -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
-69
View File
@@ -1,69 +0,0 @@
from datetime import datetime, timezone
def format_relative_time(timestamp) -> str:
"""
Convert UTC timestamp to local timezone and return relative time string.
Args:
timestamp: Either ISO format timestamp string with UTC timezone (ending with 'Z')
or Unix timestamp in milliseconds
Returns:
str: A human-readable relative time string. Possible formats:
- "X minutes ago" (when less than an hour)
- "X hours ago" (when less than a day)
- "X days ago" (when more than a day)
- "Unknown" (when timestamp is None or empty)
- "Invalid timestamp" (when parsing fails)
Note:
The function handles timezone conversion automatically, converting UTC
timestamps to the local timezone before calculating the relative time.
"""
if not timestamp:
return "Unknown"
# Convert to local timezone
try:
# First try parsing as ISO format
try:
dt = datetime.fromisoformat(str(timestamp).replace('Z', '+00:00'))
except ValueError:
# If that fails, try parsing as Unix timestamp
try:
ts = float(timestamp)
dt = datetime.fromtimestamp(ts/1000, tz=timezone.utc)
except (ValueError, TypeError):
return "Invalid timestamp"
local_dt = dt.astimezone() # Convert to local timezone
# Calculate relative time
now = datetime.now(timezone.utc).astimezone() # Get current time in local timezone
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.
Args:
role (str): The role string to format, expected in the format "prefix:role"
Returns:
str: The extracted role name (everything after the colon)
"""
split_role = role.split(":")
return f"{split_role[1]}"
@@ -1,67 +0,0 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_local_accounts, Account
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
from specklepy.core.api.models.current import Model
from typing import List, Tuple, Optional
from .misc import format_relative_time
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.
This function retrieves a list of models associated with a specific project in a Speckle account.
It authenticates with the server using the provided account credentials, validates the project existence,
and optionally filters the results based on a search string.
Args:
account_id (str): The unique identifier of the Speckle account.
project_id (str): The unique identifier of the project to fetch models from.
search (Optional[str], optional): Search string to filter models by name. Defaults to None.
Returns:
List[Tuple[str, str, str]]: A list of tuples where each tuple contains:
- model_name (str): The name of the model
- model_id (str): The unique identifier of the model
- last_updated (str): Relative time since model creation
Note:
Returns an empty list if:
- The account_id or project_id are invalid
- The account cannot be found
- The project cannot be found
- Any other error occurs during execution
Any errors encountered will be printed with an error message.
"""
try:
# Validate inputs
if not account_id or not project_id:
print(f"Error: Invalid inputs - account_id: {account_id}, project_id: {project_id}")
return []
# Get the account info
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 []
# Initialize the client
client = SpeckleClient(host=account.serverInfo.url)
# Authenticate
client.authenticate_with_account(account)
# Validate project exists
try:
client.project.get(project_id)
except Exception as e:
print(f"Error: Project with ID {project_id} not found: {str(e)}")
return []
filter = ProjectModelsFilter(search=search) if search else None
# Get models
models: List[Model] = client.model.get_models(project_id=project_id, models_limit=10, models_filter=filter).items
return [(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)}")
return []
@@ -1,55 +0,0 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_local_accounts
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
from .misc import format_relative_time, format_role
def get_projects_for_account(account_id: str, search: Optional[str] = None) -> List[Tuple[str, str, str, str]]:
"""Fetches projects for a given account from the Speckle server.
This function retrieves a list of projects associated with a specific Speckle account.
It authenticates with the server using the provided account credentials and optionally
filters the results based on a search string.
Args:
account_id (str): The unique identifier of the Speckle account.
search (Optional[str], optional): Search string to filter projects by name. Defaults to None.
Returns:
List[Tuple[str, str, str, str]]: A list of tuples where each tuple contains:
- project_name (str): The name of the project
- role (str): The user's formatted role in the project
- last_updated (str): Relative time since last update
- project_id (str): The unique identifier of the project
Note:
Returns an empty list if the account is not found or if there's an error during execution.
Any errors encountered will be printed with their full traceback.
"""
try:
# Get the account info
accounts: List[Account] = get_local_accounts()
account: Optional[Account] = next((acc for acc in accounts if acc.id == account_id), None)
if not account:
return []
# Initialize the client
client = SpeckleClient(host=account.serverInfo.url)
# Authenticate
client.authenticate_with_account(account)
# Create filter if search is provided
filter = UserProjectsFilter(search=search) if search else None
# Fetch projects
projects = client.active_user.get_projects(limit=10, filter=filter).items
return [(project.name, format_role(project.role), format_relative_time(project.updated_at), project.id) for project in projects]
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)
return []
@@ -1,92 +0,0 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_local_accounts, Account
from typing import List, Tuple, Optional
from .misc import format_relative_time
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
from specklepy.core.api.models.current import Version
def get_versions_for_model(account_id: str, project_id: str, model_id: str, search: Optional[str] = None) -> List[Tuple[str, str, str]]:
"""Fetches versions for a given model from the Speckle server.
This function retrieves a list of versions associated with a specific model in a Speckle project.
It authenticates with the server using the provided account credentials, validates the inputs,
and optionally filters the results based on a search string.
Args:
account_id (str): The unique identifier of the account.
project_id (str): The unique identifier of the project.
model_id (str): The unique identifier of the model to fetch versions from.
search (Optional[str], optional): Search string to filter versions by message. Defaults to None.
Returns:
List[Tuple[str, str, str]]: A list of tuples where each tuple contains:
- version_id (str): The unique id of the version
- message (str): The version message for the version (or "No message" if none provided)
- last_updated (str): Relative time since version creation
Note:
Returns an empty list if:
- Any of account_id, project_id, or model_id are invalid
- The account cannot be found
- Any other error occurs during execution
Any errors encountered will be printed with an error message.
"""
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 the account info
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 []
# Initialize the client
client: SpeckleClient = SpeckleClient(host=account.serverInfo.url)
# Authenticate
client.authenticate_with_account(account)
filter: ModelVersionsFilter = ModelVersionsFilter(search=search, priorityIds=[])
# Get versions
versions: List[Version] = client.version.get_versions(project_id=project_id, model_id=model_id, limit=10, filter=filter).items
return [(version.id, version.message or "No message", format_relative_time(version.created_at)) for version in versions]
except Exception as e:
print(f"Error fetching versions: {str(e)}")
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 the account info
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 ("", "", "")
# Initialize the client
client: SpeckleClient = SpeckleClient(host=account.serverInfo.url)
# Authenticate
client.authenticate_with_account(account)
# 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 or "No message", format_relative_time(latest.created_at))
except Exception as e:
print(f"Error fetching latest version: {str(e)}")
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 = "."
+975
View File
@@ -0,0 +1,975 @@
import math
from typing import Any, Collection, Dict, Iterable, List, Optional, Union
import bmesh
import bpy
from bpy.types import Collection as BCollection
from bpy.types import Object
from mathutils import Matrix as MMatrix
from mathutils import Quaternion as MQuaternion
from mathutils import Vector as MVector
from specklepy.objects.base import Base
from specklepy.objects.geometry import (
Arc,
Circle,
Curve,
Ellipse,
Line,
Mesh,
Plane,
Polycurve,
Polyline,
)
from specklepy.objects.other import BlockDefinition
from specklepy.objects.other import Collection as SCollection
from specklepy.objects.other import Instance, Transform
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.convert.util import ConversionSkippedException
from bpy_speckle.functions import _report, get_default_traversal_func, get_scale_length
from .util import (
add_colors,
add_custom_properties,
add_faces,
add_to_hierarchy,
add_uv_coords,
add_vertices,
get_render_material,
get_vertex_color_material,
render_material_to_native,
)
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 definition.id not 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 _get_revit_family_name(speckle_object)
)
def _get_revit_family_name(speckle_object: Base) -> Optional[str]:
family = getattr(speckle_object, "family", None)
family_type = getattr(speckle_object, "type", None)
if family and family_type:
return f"{family_type}-{family}"
else:
return 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
+625
View File
@@ -0,0 +1,625 @@
from typing import Dict, Iterable, List, Optional, Tuple, Union, cast
import bpy
from bpy.types import Camera as NCamera
from bpy.types import Curve as NCurve
from bpy.types import Depsgraph
from bpy.types import Mesh as NMesh
from bpy.types import MeshPolygon, Object
from deprecated import deprecated
from mathutils import Matrix as MMatrix
from mathutils import Vector as MVector
from mathutils.geometry import interpolate_bezier
from specklepy.objects import Base
from specklepy.objects.geometry import (
Box,
Curve,
Interval,
Mesh,
Point,
Polyline,
Vector,
)
from specklepy.objects.other import (
BlockDefinition,
BlockInstance,
RenderMaterial,
Transform,
)
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 is not 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)
+541
View File
@@ -0,0 +1,541 @@
import math
from typing import Any, Dict, Optional, Tuple, Union, cast
import bpy
import idprop
from bmesh.types import BMesh
from bpy.types import Collection as BCollection
from bpy.types import Material, Node, Object, ShaderNodeVertexColor
from specklepy.objects.base import Base
from specklepy.objects.geometry import Mesh
from specklepy.objects.graph_traversal.traversal import TraversalContext
from specklepy.objects.other import RenderMaterial
from bpy_speckle.convert.constants import IGNORED_PROPERTY_KEYS
from bpy_speckle.functions import _report
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 "VERTEX_COLOR" not 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
_ = 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 loop in f.loops:
luv = loop[uv_layer]
luv.uv = uv[loop.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
-2
View File
@@ -1,2 +0,0 @@
from ..converter.to_native import *
from ..converter.utils import *
File diff suppressed because it is too large Load Diff
-90
View File
@@ -1,90 +0,0 @@
from typing import Tuple, List
import bpy
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
"""
# check if material already exists with this name
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
# clear all nodes
for node in nodes:
nodes.remove(node)
# create basic Principled BSDF and output nodes
bsdf = nodes.new(type="ShaderNodeBsdfPrincipled")
output = nodes.new(type="ShaderNodeOutputMaterial")
# link shader to output
node_tree.links.new(bsdf.outputs["BSDF"], output.inputs["Surface"])
# set material properties
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
return material
+49
View File
@@ -0,0 +1,49 @@
from typing import Callable
from specklepy.objects.base import Base
from specklepy.objects.graph_traversal.traversal import GraphTraversal, TraversalRule
from specklepy.objects.units import get_scale_factor_to_meters, get_units_from_string
from bpy_speckle.convert.constants import ELEMENTS_PROPERTY_ALIASES
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])
+27 -48
View File
@@ -1,11 +1,12 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from importlib import import_module, invalidate_caches
from pathlib import Path
from typing import Optional
from importlib import import_module, invalidate_caches
_user_data_env_var = "SPECKLE_USERDATA_PATH"
@@ -55,9 +56,7 @@ 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
@@ -68,9 +67,7 @@ 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:
@@ -90,19 +87,16 @@ 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
@@ -113,7 +107,6 @@ def connector_installation_path(host_application: str) -> Path:
return connector_installation_path
def is_pip_available() -> bool:
try:
import_module("pip") # noqa F401
@@ -132,25 +125,9 @@ 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")
def is_uv_available() -> bool:
try:
import_module("uv") # noqa F401
return True
except ImportError:
return False
def ensure_uv() -> None:
print("Installing uv... ")
from subprocess import run
completed_process = run([PYTHON_PATH, "-m", "pip", "install", "uv"])
if completed_process.returncode == 0:
print("Successfully installed uv")
else:
raise Exception(f"Failed to install uv, got {completed_process.returncode} return code")
raise Exception(
f"Failed to install pip, got {completed_process.returncode} return code"
)
def get_requirements_path() -> Path:
@@ -169,12 +146,12 @@ def install_requirements(host_application: str) -> None:
def debugger_is_active() -> bool:
"""Return if the debugger is currently active"""
return hasattr(sys, 'gettrace') and sys.gettrace() is not None
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
@@ -184,11 +161,15 @@ def install_requirements(host_application: str) -> None:
[
PYTHON_PATH,
"-m",
"uv",
"pip",
"-q",
"--disable-pip-version-check",
"install",
"--system",
"--target",
"--prefer-binary",
"--ignore-installed",
"--no-compile",
"--no-deps",
"-t",
str(path),
"-r",
str(requirements_path),
@@ -198,10 +179,10 @@ def install_requirements(host_application: str) -> None:
)
if completed_process.returncode != 0:
m = f"Failed to install dependencies through uv, got {completed_process.returncode} return code"
m = f"Failed to install dependencies through pip, got {completed_process.returncode} return code"
print(m)
raise Exception(m)
print("Successfully installed dependencies")
if not is_debug:
@@ -211,9 +192,6 @@ def install_requirements(host_application: str) -> None:
def install_dependencies(host_application: str) -> None:
if not is_pip_available():
ensure_pip()
if not is_uv_available():
ensure_uv()
install_requirements(host_application)
@@ -223,7 +201,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("-", "_")
@@ -234,6 +212,7 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
@@ -241,6 +220,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}!"
)
+37
View File
@@ -0,0 +1,37 @@
from .commit import DeleteCommit
from .misc import OpenSpeckleForum, OpenSpeckleGuide, OpenSpeckleTutorials
from .streams import (
AddStreamFromURL,
CopyCommitId,
CopyModelId,
CopyStreamId,
CreateStream,
ReceiveStreamObjects,
SendStreamObjects,
ViewStreamDataApi,
)
from .users import LoadUsers, LoadUserStreams, ResetUsers
operator_classes = [
LoadUsers,
ResetUsers,
ReceiveStreamObjects,
SendStreamObjects,
LoadUserStreams,
CopyStreamId,
CopyCommitId,
CopyModelId,
]
operator_classes.extend([DeleteCommit])
operator_classes.extend(
[
ViewStreamDataApi,
AddStreamFromURL,
CreateStream,
OpenSpeckleGuide,
OpenSpeckleTutorials,
OpenSpeckleForum,
]
)
+73
View File
@@ -0,0 +1,73 @@
"""
Commit operators
"""
import bpy
from bpy.props import BoolProperty
from specklepy.logging import metrics
from bpy_speckle.clients import speckle_clients
from bpy_speckle.functions import _report
from bpy_speckle.properties.scene import get_speckle
class DeleteCommit(bpy.types.Operator):
"""
Permanently deletes the selected version from the selected model.
To execute from code, call: `bpy.ops.speckle.delete_commit(are_you_sure=True)`
"""
bl_idname = "speckle.delete_commit"
bl_label = "Delete Version"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Permanently Deletes the selected version from the selected model"
are_you_sure: BoolProperty(
name="Confirm",
default=False,
) # type: ignore
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, branch, 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"Version {commit.id} ({commit.message}) of model {branch.id} ({branch.name}) has been deleted from project {stream.id} ({stream.name})"
)
+62
View File
@@ -0,0 +1,62 @@
import webbrowser
import bpy
from specklepy.logging import metrics
class OpenSpeckleGuide(bpy.types.Operator):
_guide_url = "https://speckle.guide/user/blender.html"
bl_idname = "speckle.open_speckle_guide"
bl_label = "Speckle Docs"
bl_options = {"REGISTER", "UNDO"}
bl_description = f"Browse the documentation on the Speckle Guide ({_guide_url})"
def execute(self, context):
webbrowser.open(self._guide_url)
metrics.track(
"Connector Action",
None,
custom_props={"name": "OpenSpeckleGuide"},
)
return {"FINISHED"}
class OpenSpeckleTutorials(bpy.types.Operator):
_tutorials_url = "https://speckle.systems/tutorials/"
bl_idname = "speckle.open_speckle_tutorials"
bl_label = "Tutorials Portal"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
f"Visit our tutorials portal for learning resources ({_tutorials_url})"
)
def execute(self, context):
webbrowser.open(self._tutorials_url)
metrics.track(
"Connector Action",
None,
custom_props={"name": "OpenSpeckleTutorials"},
)
return {"FINISHED"}
class OpenSpeckleForum(bpy.types.Operator):
_forum_url = "https://speckle.community/"
bl_idname = "speckle.open_speckle_forum"
bl_label = "Community Forum"
bl_options = {"REGISTER", "UNDO"}
bl_description = (
f"Ask questions and join the discussion on our community forum ({_forum_url})"
)
def execute(self, context):
webbrowser.open(self._forum_url)
metrics.track(
"Connector Action",
None,
custom_props={"name": "OpenSpeckleForum"},
)
return {"FINISHED"}
+737
View File
@@ -0,0 +1,737 @@
"""
Stream operators
"""
import webbrowser
from math import radians
from typing import Callable, Dict, Optional, Tuple, Union, cast
import bpy
from bpy.props import BoolProperty, EnumProperty, StringProperty
from bpy.types import Collection, Context, Object
from deprecated import deprecated
from specklepy.core.api import host_applications, operations
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.models import Commit, Stream
from specklepy.core.api.wrapper import StreamWrapper
from specklepy.logging import metrics
from specklepy.logging.exceptions import SpeckleException
from specklepy.objects import Base
from specklepy.objects.other import Collection as SCollection
from specklepy.transports.server import ServerTransport
from bpy_speckle.blender_commit_object_builder import BlenderCommitObjectBuilder
from bpy_speckle.clients import speckle_clients
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.convert.util import ConversionSkippedException, add_to_hierarchy
from bpy_speckle.functions import _report, get_default_traversal_func, get_scale_length
from bpy_speckle.operators.users import LoadUserStreams, add_user_stream
from bpy_speckle.properties.scene import (
SpeckleSceneSettings,
SpeckleStreamObject,
SpeckleUserObject,
get_speckle,
selection_state,
)
ObjectCallback = Optional[Callable[[bpy.types.Context, Object, Base], Object]]
ReceiveCompleteCallback = Optional[
Callable[[bpy.types.Context, Dict[str, Union[Object, Collection]]], None]
]
def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional[str]:
workspace_id = None
server_version = client.project.server_version or client.server.version()
# Local yarn builds of server will report a server version if "dev"
# We'll assume that local builds are up-to-date with the latest features
if server_version[0] == "dev":
maj = 999
min = 999
else:
maj = server_version[0]
min = server_version[1]
if maj > 2 or (maj == 2 and min > 20):
workspace_id = client.project.get(project_id).workspaceId
return workspace_id
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 objects from selected model version
"""
bl_idname = "speckle.receive_stream_objects"
bl_label = "Receive"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Receive objects from selected model version"
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 is 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 model version 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,
"workspace_id": get_project_workspace_id(client, stream.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 = f"{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 selected objects to selected model
"""
bl_idname = "speckle.send_stream_objects"
bl_label = "Send"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Send selected objects to selected model"
apply_modifiers: BoolProperty(name="Apply modifiers", default=True) # type: ignore
commit_message: StringProperty(
name="Message",
default="Sent 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"Sent {N} element from Blender."
else:
self.commit_message = f"Sent {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",
"workspace_id": get_project_workspace_id(client, stream.id),
},
)
_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}")
selection_state.selected_commit_id = COMMIT_ID
selection_state.selected_branch_id = branch.id
selection_state.selected_stream_id = stream.id
selection_state.selected_user_id = user.id
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 Model in Web"
bl_options = {"REGISTER", "UNDO"}
bl_description = "View the selected model 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)
url = self._get_url_from_selection(speckle)
_report(f"Opening {url} in web browser")
if not webbrowser.open(url, new=2):
raise Exception(f"Failed to open model in browser ({url})")
metrics.track(
"Connector Action",
None,
custom_props={"name": "view_stream_data_api"},
)
@staticmethod
def _get_url_from_selection(speckleScene: SpeckleSceneSettings) -> str:
client = speckle_clients[int(speckleScene.active_user)]
(user, stream) = speckleScene.validate_stream_selection()
branch = stream.get_active_branch()
commit = branch.get_active_commit() if branch else None
if client.account.serverInfo.frontend2:
server_url = f"{user.server_url}/projects/{stream.id}/"
if branch:
server_url += f"models/{branch.id}"
if commit:
server_url += f"@{commit.id}"
else:
server_url = f"{user.server_url}/streams/{stream.id}/"
if commit:
server_url += f"commits/{commit.id}"
elif branch:
server_url += f"branches/{branch.name}"
return server_url
class AddStreamFromURL(bpy.types.Operator):
"""
Add / select an existing project by providing its URL
"""
bl_idname = "speckle.add_stream_from_url"
bl_label = "Add Project From URL"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add / select an existing project by providing its URL"
stream_url: StringProperty(name="Project URL", default="") # 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 project {wrapper.stream_id}"
)
(index, b_stream) = self._get_or_add_stream(user, stream)
user.active_stream = index
_report(f"Selecting project 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 a new Speckle project using the selected user account
"""
bl_idname = "speckle.create_stream"
bl_label = "Create Project"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Create a new Speckle project using the selected user account"
stream_name: StringProperty(name="Project name") # type: ignore
stream_description: StringProperty(
name="Project description", default="My new project"
) # 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 CopyStreamId(bpy.types.Operator):
"""
Copy the selected project id to clipboard
"""
bl_idname = "speckle.stream_copy_id"
bl_label = "Copy Project Id"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy the selected project 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 the selected version id to clipboard
"""
bl_idname = "speckle.commit_copy_id"
bl_label = "Copy Version Id"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy the selected version 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 CopyModelId(bpy.types.Operator):
"""
Copy model id to clipboard
"""
bl_idname = "speckle.model_copy_id"
bl_label = "Copy model id"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Copy model id to clipboard"
def execute(self, context):
self.copy_model_id(context)
return {"FINISHED"}
def copy_model_id(self, context) -> None:
speckle = get_speckle(context)
(_, _, branch) = speckle.validate_branch_selection()
bpy.context.window_manager.clipboard = branch.id
metrics.track(
"Connector Action",
custom_props={"name": "copy_branch_id"},
)
+224
View File
@@ -0,0 +1,224 @@
"""
User account operators
"""
from typing import cast
import bpy
from bpy.types import Context
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_local_accounts
from specklepy.core.api.models import Stream
from specklepy.logging import metrics
from bpy_speckle.clients import speckle_clients
from bpy_speckle.functions import _report
from bpy_speckle.properties.scene import (
SpeckleSceneSettings,
SpeckleStreamObject,
SpeckleUserObject,
get_speckle,
restore_selection_state,
)
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):
"""
Loads all user accounts from the credentials in the local database.
See docs to add accounts via Manager
"""
bl_idname = "speckle.users_load"
bl_label = "Load Users"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Loads all user accounts from the credentials in the local database.\nSee docs to add accounts via Manager"
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 stream.branches:
s.load_stream_branches(stream)
class LoadUserStreams(bpy.types.Operator):
"""
(Re)Load all available projects for active user
"""
bl_idname = "speckle.load_user_streams"
bl_label = "Load User's Projects"
bl_options = {"REGISTER", "UNDO"}
bl_description = "(Re)Load all available projects for active user"
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("Failed to retrieve projects") from ex
if not streams:
_report("Zero projects found")
return
active_stream_id = None
if active_stream := user.get_active_stream():
active_stream_id = active_stream.id
elif len(user.streams) > 0:
active_stream_id = user.streams[0].id
user.streams.clear()
for i, s in enumerate(streams):
assert s.id
load_branches = s.id == active_stream_id if active_stream_id else i == 0
if load_branches:
sstream = client.stream.get(
id=s.id, branch_limit=self.branch_limit, commit_limit=10
)
add_user_stream(user, sstream)
else:
add_user_stream(user, s)
restore_selection_state(speckle)
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 .addon import SpeckleAddonPreferences
from .collection import SpeckleCollectionSettings
from .object import SpeckleObjectSettings
from .scene import (
SpeckleBranchObject,
SpeckleCommitObject,
SpeckleSceneObject,
SpeckleSceneSettings,
SpeckleStreamObject,
SpeckleUserObject,
)
property_classes = [
SpeckleSceneObject,
SpeckleCommitObject,
SpeckleBranchObject,
SpeckleStreamObject,
SpeckleUserObject,
SpeckleSceneSettings,
SpeckleObjectSettings,
SpeckleCollectionSettings,
SpeckleAddonPreferences,
]
+18
View File
@@ -0,0 +1,18 @@
"""
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") # type: ignore
send_or_receive: bpy.props.EnumProperty(
name="Mode",
items=(
("send", "Send", "Send data to Speckle server."),
("receive", "Receive", "Receive data from Speckle server."),
),
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
name: bpy.props.StringProperty(default="") # type: ignore
+19
View File
@@ -0,0 +1,19 @@
"""
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."),
),
) # type: ignore
stream_id: bpy.props.StringProperty(default="") # type: ignore
object_id: bpy.props.StringProperty(default="") # type: ignore
+364
View File
@@ -0,0 +1,364 @@
"""
Scene properties
"""
from dataclasses import dataclass
from typing import Iterable, Optional, Tuple, Union, cast
import bpy
from bpy.props import (
CollectionProperty,
EnumProperty,
FloatProperty,
IntProperty,
StringProperty,
)
from specklepy.core.api.models import Stream
from bpy_speckle.clients import speckle_clients
class SpeckleSceneObject(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(default="") # type: ignore
class SpeckleCommitObject(bpy.types.PropertyGroup):
id: StringProperty(default="") # type: ignore
message: StringProperty(default="") # type: ignore
author_name: StringProperty(default="") # type: ignore
author_id: StringProperty(default="") # type: ignore
created_at: StringProperty(default="") # type: ignore
source_application: StringProperty(default="") # type: ignore
referenced_object: StringProperty(default="") # type: ignore
class SpeckleBranchObject(bpy.types.PropertyGroup):
def get_commits(self, context):
if self.commits is not None and len(self.commits) > 0:
COMMITS = cast(Iterable[SpeckleCommitObject], self.commits)
return [
(str(i), commit.id, commit.message, i)
for i, commit in enumerate(COMMITS)
]
return [("0", "<none>", "<none>", 0)]
def commit_update_hook(self, context: bpy.types.Context):
selection_state.selected_commit_id = SelectionState.get_item_id_by_index(
self.commits, self.commit
)
selection_state.selected_branch_id = self.id
# print(f"commit_update_hook: {selection_state.selected_commit_id=}, {selection_state.selected_branch_id=}")
name: StringProperty(default="main") # type: ignore
id: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
commits: CollectionProperty(type=SpeckleCommitObject) # type: ignore
commit: EnumProperty(
name="Version",
description="Selected model version",
items=get_commits,
update=commit_update_hook,
) # type: ignore
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 load_stream_branches(self, sstream: Stream):
self.branches.clear()
# branches = [branch for branch in stream.branches.items if branch.name != "globals"]
for b in sstream.branches.items:
branch = cast(SpeckleBranchObject, self.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
def get_branches(self, context):
if self.branches:
BRANCHES = cast(Iterable[SpeckleBranchObject], self.branches)
return [
(str(i), branch.name, branch.description, i)
for i, branch in enumerate(BRANCHES)
if branch.name != "globals"
]
return [("0", "<none>", "<none>", 0)]
def branch_update_hook(self, context: bpy.types.Context):
selection_state.selected_branch_id = SelectionState.get_item_id_by_index(
self.branches, self.branch
)
# print(f"branch_update_hook: {selection_state.selected_branch_id=}, {selection_state.selected_stream_id=}")
name: StringProperty(default="") # type: ignore
description: StringProperty(default="") # type: ignore
id: StringProperty(default="") # type: ignore
branches: CollectionProperty(type=SpeckleBranchObject) # type: ignore
branch: EnumProperty(
name="Model",
description="Selected Model",
items=get_branches,
update=branch_update_hook,
) # type: ignore
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):
def fetch_stream_branches(
self, context: bpy.types.Context, stream: SpeckleStreamObject
):
speckle = context.scene.speckle
client = speckle_clients[int(speckle.active_user)]
sstream = client.stream.get(
id=stream.id, branch_limit=100, commit_limit=10
) # TODO: refactor magic numbers
stream.load_stream_branches(sstream)
def stream_update_hook(self, context: bpy.types.Context):
stream = SelectionState.get_item_by_index(self.streams, self.active_stream)
selection_state.selected_stream_id = stream.id
# print(f"stream_update_hook: {selection_state.selected_stream_id=}, {selection_state.selected_user_id=}")
if (
len(stream.branches) == 0
): # do not reload on selection, same as the old behavior
self.fetch_stream_branches(context, stream)
server_name: StringProperty(default="SpeckleXYZ") # type: ignore
server_url: StringProperty(default="https://speckle.xyz") # type: ignore
id: StringProperty(default="") # type: ignore
name: StringProperty(default="Speckle User") # type: ignore
email: StringProperty(default="user@speckle.xyz") # type: ignore
company: StringProperty(default="SpeckleSystems") # type: ignore
streams: CollectionProperty(type=SpeckleStreamObject) # type: ignore
active_stream: IntProperty(default=0, update=stream_update_hook) # type: ignore
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=[],
) # type: ignore
users: CollectionProperty(type=SpeckleUserObject) # type: ignore
def get_users(self, context):
USERS = cast(Iterable[SpeckleUserObject], self.users)
return [
(str(i), f"{user.email} ({user.server_name})", user.server_url, i)
for i, user in enumerate(USERS)
]
def user_update_hook(self, context):
bpy.ops.speckle.load_user_streams() # type: ignore
selection_state.selected_user_id = SelectionState.get_item_id_by_index(
self.users, self.active_user
)
active_user: EnumProperty(
items=get_users,
name="Account",
description="Select account",
update=user_update_hook,
get=None,
set=None,
) # type: ignore
objects: CollectionProperty(type=SpeckleSceneObject) # type: ignore
scale: FloatProperty(default=0.001) # type: ignore
user: StringProperty(
name="User",
description="Current user",
default="Speckle User",
) # type: ignore
receive_script: EnumProperty(
name="Receive script",
description="Custom py script to execute when receiving objects. See docs for function signature.",
items=get_scripts,
) # type: ignore
send_script: EnumProperty(
name="Send script",
description="Custom py script to execute when sending objects. See docs for function signature",
items=get_scripts,
) # type: ignore
def get_active_user(self) -> Optional[SpeckleUserObject]:
if self.active_user is None:
return None
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 account 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 project 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 model 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 model version selected/found")
return (user, stream, branch, commit)
class SelectionException(Exception):
pass
def get_speckle(context: bpy.types.Context) -> SpeckleSceneSettings:
"""
Gets the speckle scene object
"""
return context.scene.speckle # type: ignore
@dataclass
class SelectionState:
selected_user_id: Optional[str] = None
selected_stream_id: Optional[str] = None
selected_branch_id: Optional[str] = None
selected_commit_id: Optional[str] = None
@staticmethod
def get_item_id_by_index(
collection: bpy.types.PropertyGroup, index: Union[str, int]
) -> Optional[str]:
if item := SelectionState.get_item_by_index(collection, index):
return item.id
return None
@staticmethod
def get_item_by_index(
collection: bpy.types.PropertyGroup, index: Union[str, int]
) -> Optional[bpy.types.PropertyGroup]:
items = collection.values()
i = int(index)
if 0 <= i <= len(items):
return items[i]
return None
@staticmethod
def get_item_index_by_id(
collection: Iterable[SpeckleCommitObject], id: Optional[str]
) -> Optional[str]:
for index, item in enumerate(collection):
if item.id == id:
return str(index)
return None
selection_state = SelectionState()
def restore_selection_state(speckle: SpeckleSceneSettings) -> None:
# Restore branch selection state
if selection_state.selected_branch_id is not None:
(active_user, active_stream) = speckle.validate_stream_selection()
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}")
# print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
is_same_user = active_user.id == selection_state.selected_user_id
if is_same_user:
active_user.active_stream = int(
SelectionState.get_item_index_by_id(
active_user.streams, selection_state.selected_stream_id
)
)
active_stream = SelectionState.get_item_by_index(
active_user.streams, active_user.active_stream
)
if branch := SelectionState.get_item_index_by_id(
active_stream.branches, selection_state.selected_branch_id
):
active_stream.branch = branch
# Restore commit selection state
if selection_state.selected_commit_id is not None:
(
active_user,
active_stream,
active_branch,
) = speckle.validate_branch_selection()
# print(f"restore_selection_state: {active_user.id=}, {active_stream.id=}, {active_branch.id=}")
# print(f"restore_selection_state: {selection_state.selected_user_id=}, {selection_state.selected_stream_id=}, {selection_state.selected_branch_id=}, {selection_state.selected_commit_id=}")
is_same_user = active_user.id == selection_state.selected_user_id
is_same_stream = active_stream.id == selection_state.selected_stream_id
is_same_branch = active_branch.id == selection_state.selected_branch_id
if is_same_user and is_same_stream and is_same_branch:
if commit := SelectionState.get_item_index_by_id(
active_branch.commits, selection_state.selected_commit_id
):
active_branch.commit = commit
+18
View File
@@ -0,0 +1,18 @@
from .object import OBJECT_PT_speckle
from .view3d import (
VIEW3D_PT_SpeckleActiveStream,
VIEW3D_PT_SpeckleHelp,
VIEW3D_PT_SpeckleStreams,
VIEW3D_PT_SpeckleUser,
VIEW3D_UL_SpeckleStreams,
VIEW3D_UL_SpeckleUsers,
)
ui_classes = [
VIEW3D_PT_SpeckleUser,
VIEW3D_PT_SpeckleStreams,
VIEW3D_PT_SpeckleActiveStream,
VIEW3D_UL_SpeckleUsers,
VIEW3D_UL_SpeckleStreams,
VIEW3D_PT_SpeckleHelp,
]
+30
View File
@@ -0,0 +1,30 @@
"""
Object UI elements
"""
import bpy
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 (Legacy)"
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="Project 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")
+267
View File
@@ -0,0 +1,267 @@
"""
Speckle UI elements for the 3d viewport
"""
from datetime import datetime
import bpy
from bpy_speckle.properties.scene import get_speckle
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 projects 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="Projects", 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 (Legacy)"
bl_context = "objectmode"
bl_label = "User Account"
def draw(self, context):
speckle = get_speckle(context)
layout = self.layout
# Draw a box with the text "Warning: This is a legacy add-on. Download the new connector from https://app.speckle.systems/connectors"
box = layout.box()
box.label(text="Warning: This is a legacy add-on.", icon="ERROR")
box.label(text="Download the new connector from")
box.label(text="https://app.speckle.systems/connectors")
layout.separator()
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=f"{user.server_name} ({user.server_url})")
col.label(text=f"{user.name} ({user.email})")
col.operator("speckle.users_load", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleStreams(bpy.types.Panel):
"""
Speckle projects UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle (Legacy)"
bl_context = "objectmode"
bl_label = "Projects"
def draw(self, context):
speckle = get_speckle(context)
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No Projects")
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.load_user_streams", text="", icon="FILE_REFRESH")
class VIEW3D_PT_SpeckleActiveStream(bpy.types.Panel):
"""
Speckle Active Projects UI panel in the 3d viewport
"""
bl_space_type = "VIEW_3D"
bl_region_type = Region
bl_category = "Speckle (Legacy)"
bl_context = "objectmode"
bl_label = "Active Project"
def draw(self, context):
speckle = get_speckle(context)
col = self.layout.column()
if len(speckle.users) < 1:
col.label(text="No projects")
else:
user = speckle.validate_user_selection()
# user = speckle.users[int(speckle.active_user)]
if len(user.streams) < 1:
col.label(text="No active project")
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="Model")
row.operator("speckle.model_copy_id", text="", icon="COPY_ID")
if len(stream.branches) > 0:
branch = stream.branches[int(stream.branch)]
row = col.row()
row.prop(branch, "commit", text="Version")
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 models 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="")
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 Model 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 (Legacy)"
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 --all-extras
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)
+11 -26
View File
@@ -1,47 +1,32 @@
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
+1534
View File
File diff suppressed because it is too large Load Diff
+46 -12
View File
@@ -1,16 +1,50 @@
[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.0a3"
]
package-mode = false
[dependency-groups]
dev = [
"fake-bpy-module-latest>=20240524,<20240525",
"ruff>=0.4.4,<0.5",
]
[tool.poetry.requires-plugins]
poetry-plugin-export = ">=1.8"
[tool.poetry.dependencies]
python = ">=3.9.0, <4.0.0"
specklepy = "^2.23.0"
# [tool.poetry.group.local_specklepy.dependencies]
# specklepy = {path = "../specklepy", develop = true}
[tool.poetry.group.dev.dependencies]
fake-bpy-module-latest = "^20241010"
black = "24.10.0"
isort = "^5.13.2"
pylint = "^3.3.2"
ruff = "^0.8.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
include = '\.pyi?$'
line-length = 88
target-version = ["py39", "py310", "py311", "py312", "py313"]
[tool.isort]
profile = "black"
Generated
-707
View File
@@ -1,707 +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.5.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766 },
]
[[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.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/69/82/3c4e1d44f3cbaa2a578127d641fe385ba3bff6c38b789447ae11a21fa413/attrs-25.2.0.tar.gz", hash = "sha256:18a06db706db43ac232cce80443fcd9f2500702059ecf53489e3c5a3f417acaf", size = 812038 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/33/7a7388b9ef94aab40539939d94461ec682afbd895458945ed25be07f03f6/attrs-25.2.0-py3-none-any.whl", hash = "sha256:611344ff0a5fed735d86d7784610c84f8126b95e549bcad9ff61b4242f2d386b", size = 64016 },
]
[[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 = "2024.8.30"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 },
{ url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 },
{ url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 },
{ url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 },
{ url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 },
{ url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 },
{ url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 },
{ url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 },
{ url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 },
{ url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 },
{ url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 },
{ url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 },
{ url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 },
{ url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 },
{ url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 },
{ url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 },
{ url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 },
{ url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 },
{ url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 },
{ url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 },
{ url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 },
{ url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 },
{ url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 },
{ url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 },
{ url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 },
{ url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 },
{ url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 },
{ url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 },
{ url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 },
{ url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 },
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
{ url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 },
{ url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 },
{ url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 },
{ url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 },
{ url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 },
{ url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 },
{ url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 },
{ url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 },
{ url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 },
{ url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 },
{ url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 },
{ url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 },
{ url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
]
[[package]]
name = "deprecated"
version = "1.2.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/a3/53e7d78a6850ffdd394d7048a31a6f14e44900adedf190f9a165f6b69439/deprecated-1.2.15.tar.gz", hash = "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d", size = 2977612 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941 },
]
[[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.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "backoff" },
{ name = "graphql-core" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/85/feda24b33adcc6c8463a62a8e2ca2cc3425dc6d687388ff728ceae231204/gql-3.5.0.tar.gz", hash = "sha256:ccb9c5db543682b28f577069950488218ed65d4ac70bb03b6929aaadaf636de9", size = 179939 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/fb/01a200e1c31b79690427c8e983014e4220d2652b4372a46fe4598e1d7a8e/gql-3.5.0-py2.py3-none-any.whl", hash = "sha256:70dda5694a5b194a8441f077aa5fb70cc94e4ec08016117523f013680901ecb7", size = 74001 },
]
[package.optional-dependencies]
requests = [
{ name = "requests" },
{ name = "requests-toolbelt" },
]
websockets = [
{ name = "websockets" },
]
[[package]]
name = "graphql-core"
version = "3.2.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/b5/ebc6fe3852e2d2fdaf682dddfc366934f3d2c9ef9b6d1b0e6ca348d936ba/graphql_core-3.2.5.tar.gz", hash = "sha256:e671b90ed653c808715645e3998b7ab67d382d55467b7e2978549111bbabf8d5", size = 504664 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/dc/078bd6b304de790618ebb95e2aedaadb78f4527ac43a9ad8815f006636b6/graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a", size = 203189 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[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.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 },
{ url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 },
{ url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 },
{ url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 },
{ url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 },
{ url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 },
{ url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 },
{ url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 },
{ url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 },
{ url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 },
{ url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 },
{ url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 },
{ url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 },
{ url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 },
{ url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 },
{ url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 },
{ url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 },
{ url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 },
{ url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 },
{ url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 },
{ url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 },
{ url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 },
{ url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 },
{ url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 },
{ url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 },
{ url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 },
{ url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 },
{ url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 },
{ url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 },
{ url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 },
{ url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 },
{ url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 },
{ url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 },
{ url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 },
{ url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 },
{ url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 },
{ url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 },
{ url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 },
{ url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 },
{ url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 },
{ url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 },
{ url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 },
{ url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 },
{ url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 },
{ url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 },
{ url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 },
]
[[package]]
name = "propcache"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/1c/71eec730e12aec6511e702ad0cd73c2872eccb7cad39de8ba3ba9de693ef/propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", size = 80811 },
{ url = "https://files.pythonhosted.org/packages/89/c3/7e94009f9a4934c48a371632197406a8860b9f08e3f7f7d922ab69e57a41/propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", size = 46365 },
{ url = "https://files.pythonhosted.org/packages/c0/1d/c700d16d1d6903aeab28372fe9999762f074b80b96a0ccc953175b858743/propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", size = 45602 },
{ url = "https://files.pythonhosted.org/packages/2e/5e/4a3e96380805bf742712e39a4534689f4cddf5fa2d3a93f22e9fd8001b23/propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", size = 236161 },
{ url = "https://files.pythonhosted.org/packages/a5/85/90132481183d1436dff6e29f4fa81b891afb6cb89a7306f32ac500a25932/propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", size = 244938 },
{ url = "https://files.pythonhosted.org/packages/4a/89/c893533cb45c79c970834274e2d0f6d64383ec740be631b6a0a1d2b4ddc0/propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", size = 243576 },
{ url = "https://files.pythonhosted.org/packages/8c/56/98c2054c8526331a05f205bf45cbb2cda4e58e56df70e76d6a509e5d6ec6/propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", size = 236011 },
{ url = "https://files.pythonhosted.org/packages/2d/0c/8b8b9f8a6e1abd869c0fa79b907228e7abb966919047d294ef5df0d136cf/propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504", size = 224834 },
{ url = "https://files.pythonhosted.org/packages/18/bb/397d05a7298b7711b90e13108db697732325cafdcd8484c894885c1bf109/propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", size = 224946 },
{ url = "https://files.pythonhosted.org/packages/25/19/4fc08dac19297ac58135c03770b42377be211622fd0147f015f78d47cd31/propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", size = 217280 },
{ url = "https://files.pythonhosted.org/packages/7e/76/c79276a43df2096ce2aba07ce47576832b1174c0c480fe6b04bd70120e59/propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", size = 220088 },
{ url = "https://files.pythonhosted.org/packages/c3/9a/8a8cf428a91b1336b883f09c8b884e1734c87f724d74b917129a24fe2093/propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", size = 233008 },
{ url = "https://files.pythonhosted.org/packages/25/7b/768a8969abd447d5f0f3333df85c6a5d94982a1bc9a89c53c154bf7a8b11/propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", size = 237719 },
{ url = "https://files.pythonhosted.org/packages/ed/0d/e5d68ccc7976ef8b57d80613ac07bbaf0614d43f4750cf953f0168ef114f/propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", size = 227729 },
{ url = "https://files.pythonhosted.org/packages/05/64/17eb2796e2d1c3d0c431dc5f40078d7282f4645af0bb4da9097fbb628c6c/propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", size = 40473 },
{ url = "https://files.pythonhosted.org/packages/83/c5/e89fc428ccdc897ade08cd7605f174c69390147526627a7650fb883e0cd0/propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", size = 44921 },
{ url = "https://files.pythonhosted.org/packages/7c/46/a41ca1097769fc548fc9216ec4c1471b772cc39720eb47ed7e38ef0006a9/propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", size = 80800 },
{ url = "https://files.pythonhosted.org/packages/75/4f/93df46aab9cc473498ff56be39b5f6ee1e33529223d7a4d8c0a6101a9ba2/propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", size = 46443 },
{ url = "https://files.pythonhosted.org/packages/0b/17/308acc6aee65d0f9a8375e36c4807ac6605d1f38074b1581bd4042b9fb37/propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", size = 45676 },
{ url = "https://files.pythonhosted.org/packages/65/44/626599d2854d6c1d4530b9a05e7ff2ee22b790358334b475ed7c89f7d625/propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", size = 246191 },
{ url = "https://files.pythonhosted.org/packages/f2/df/5d996d7cb18df076debae7d76ac3da085c0575a9f2be6b1f707fe227b54c/propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", size = 251791 },
{ url = "https://files.pythonhosted.org/packages/2e/6d/9f91e5dde8b1f662f6dd4dff36098ed22a1ef4e08e1316f05f4758f1576c/propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", size = 253434 },
{ url = "https://files.pythonhosted.org/packages/3c/e9/1b54b7e26f50b3e0497cd13d3483d781d284452c2c50dd2a615a92a087a3/propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", size = 248150 },
{ url = "https://files.pythonhosted.org/packages/a7/ef/a35bf191c8038fe3ce9a414b907371c81d102384eda5dbafe6f4dce0cf9b/propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", size = 233568 },
{ url = "https://files.pythonhosted.org/packages/97/d9/d00bb9277a9165a5e6d60f2142cd1a38a750045c9c12e47ae087f686d781/propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", size = 229874 },
{ url = "https://files.pythonhosted.org/packages/8e/78/c123cf22469bdc4b18efb78893e69c70a8b16de88e6160b69ca6bdd88b5d/propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", size = 225857 },
{ url = "https://files.pythonhosted.org/packages/31/1b/fd6b2f1f36d028820d35475be78859d8c89c8f091ad30e377ac49fd66359/propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", size = 227604 },
{ url = "https://files.pythonhosted.org/packages/99/36/b07be976edf77a07233ba712e53262937625af02154353171716894a86a6/propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", size = 238430 },
{ url = "https://files.pythonhosted.org/packages/0d/64/5822f496c9010e3966e934a011ac08cac8734561842bc7c1f65586e0683c/propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", size = 244814 },
{ url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 },
{ url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 },
{ url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 },
{ url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 },
{ url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 },
{ url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 },
{ url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 },
{ url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 },
{ url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 },
{ url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 },
{ url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 },
{ url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 },
{ url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 },
{ url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 },
{ url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 },
{ url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 },
{ url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 },
{ url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 },
{ url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 },
{ url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 },
{ url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 },
{ url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 },
{ url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 },
{ url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 },
{ url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 },
{ url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 },
{ url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 },
{ url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 },
{ url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 },
{ url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 },
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 },
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 },
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 },
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "pydantic-settings"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[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.0a3" }]
[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.0a3"
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 = "stringcase" },
{ name = "ujson" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/83/e4d9bb86c3a00dbbe00db53ace96710439a5f208440898ba2fe152874d72/specklepy-3.0.0a3.tar.gz", hash = "sha256:27f8b4553e748251d09a39fa2d4f75bfedaed46bff457730e8b2b73654a041b9", size = 199614 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/30/89e57b11b6908057c95e015fe66f024d8c60fbb50368d9fb8608e316cdf8/specklepy-3.0.0a3-py3-none-any.whl", hash = "sha256:6d342c138807b310d3b27995cde6ac955cba4c2e6735b01dee3376f9e1db874d", size = 110320 },
]
[[package]]
name = "stringcase"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/1f/1241aa3d66e8dc1612427b17885f5fcd9c9ee3079fc0d28e9a3aeeb36fa3/stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008", size = 2958 }
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[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.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
]
[[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.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/a1/fc03dca9b0432725c2e8cdbf91a349d2194cf03d8523c124faebe581de09/wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801", size = 55542 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/40/def56538acddc2f764c157d565b9f989072a1d2f2a8e384324e2e104fc7d/wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a", size = 38766 },
{ url = "https://files.pythonhosted.org/packages/89/e2/8c299f384ae4364193724e2adad99f9504599d02a73ec9199bf3f406549d/wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed", size = 83730 },
{ url = "https://files.pythonhosted.org/packages/29/ef/fcdb776b12df5ea7180d065b28fa6bb27ac785dddcd7202a0b6962bbdb47/wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489", size = 75470 },
{ url = "https://files.pythonhosted.org/packages/55/b5/698bd0bf9fbb3ddb3a2feefbb7ad0dea1205f5d7d05b9cbab54f5db731aa/wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9", size = 83168 },
{ url = "https://files.pythonhosted.org/packages/ce/07/701a5cee28cb4d5df030d4b2649319e36f3d9fdd8000ef1d84eb06b9860d/wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339", size = 82307 },
{ url = "https://files.pythonhosted.org/packages/42/92/c48ba92cda6f74cb914dc3c5bba9650dc80b790e121c4b987f3a46b028f5/wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d", size = 75101 },
{ url = "https://files.pythonhosted.org/packages/8a/0a/9276d3269334138b88a2947efaaf6335f61d547698e50dff672ade24f2c6/wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b", size = 81835 },
{ url = "https://files.pythonhosted.org/packages/b9/4c/39595e692753ef656ea94b51382cc9aea662fef59d7910128f5906486f0e/wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346", size = 36412 },
{ url = "https://files.pythonhosted.org/packages/63/bb/c293a67fb765a2ada48f48cd0f2bb957da8161439da4c03ea123b9894c02/wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a", size = 38744 },
{ url = "https://files.pythonhosted.org/packages/85/82/518605474beafff11f1a34759f6410ab429abff9f7881858a447e0d20712/wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569", size = 38904 },
{ url = "https://files.pythonhosted.org/packages/80/6c/17c3b2fed28edfd96d8417c865ef0b4c955dc52c4e375d86f459f14340f1/wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504", size = 88622 },
{ url = "https://files.pythonhosted.org/packages/4a/11/60ecdf3b0fd3dca18978d89acb5d095a05f23299216e925fcd2717c81d93/wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451", size = 80920 },
{ url = "https://files.pythonhosted.org/packages/d2/50/dbef1a651578a3520d4534c1e434989e3620380c1ad97e309576b47f0ada/wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1", size = 89170 },
{ url = "https://files.pythonhosted.org/packages/44/a2/78c5956bf39955288c9e0dd62e807b308c3aa15a0f611fbff52aa8d6b5ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106", size = 86748 },
{ url = "https://files.pythonhosted.org/packages/99/49/2ee413c78fc0bdfebe5bee590bf3becdc1fab0096a7a9c3b5c9666b2415f/wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada", size = 79734 },
{ url = "https://files.pythonhosted.org/packages/c0/8c/4221b7b270e36be90f0930fe15a4755a6ea24093f90b510166e9ed7861ea/wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4", size = 87552 },
{ url = "https://files.pythonhosted.org/packages/4c/6b/1aaccf3efe58eb95e10ce8e77c8909b7a6b0da93449a92c4e6d6d10b3a3d/wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635", size = 36647 },
{ url = "https://files.pythonhosted.org/packages/b3/4f/243f88ac49df005b9129194c6511b3642818b3e6271ddea47a15e2ee4934/wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7", size = 38830 },
{ url = "https://files.pythonhosted.org/packages/67/9c/38294e1bb92b055222d1b8b6591604ca4468b77b1250f59c15256437644f/wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181", size = 38904 },
{ url = "https://files.pythonhosted.org/packages/78/b6/76597fb362cbf8913a481d41b14b049a8813cd402a5d2f84e57957c813ae/wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393", size = 88608 },
{ url = "https://files.pythonhosted.org/packages/bc/69/b500884e45b3881926b5f69188dc542fb5880019d15c8a0df1ab1dfda1f7/wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4", size = 80879 },
{ url = "https://files.pythonhosted.org/packages/52/31/f4cc58afe29eab8a50ac5969963010c8b60987e719c478a5024bce39bc42/wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b", size = 89119 },
{ url = "https://files.pythonhosted.org/packages/aa/9c/05ab6bf75dbae7a9d34975fb6ee577e086c1c26cde3b6cf6051726d33c7c/wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721", size = 86778 },
{ url = "https://files.pythonhosted.org/packages/0e/6c/4b8d42e3db355603d35fe5c9db79c28f2472a6fd1ccf4dc25ae46739672a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90", size = 79793 },
{ url = "https://files.pythonhosted.org/packages/69/23/90e3a2ee210c0843b2c2a49b3b97ffcf9cad1387cb18cbeef9218631ed5a/wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a", size = 87606 },
{ url = "https://files.pythonhosted.org/packages/5f/06/3683126491ca787d8d71d8d340e775d40767c5efedb35039d987203393b7/wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045", size = 36651 },
{ url = "https://files.pythonhosted.org/packages/f1/bc/3bf6d2ca0d2c030d324ef9272bea0a8fdaff68f3d1fa7be7a61da88e51f7/wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838", size = 38835 },
{ url = "https://files.pythonhosted.org/packages/ce/b5/251165c232d87197a81cd362eeb5104d661a2dd3aa1f0b33e4bf61dda8b8/wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b", size = 40146 },
{ url = "https://files.pythonhosted.org/packages/89/33/1e1bdd3e866eeb73d8c4755db1ceb8a80d5bd51ee4648b3f2247adec4e67/wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379", size = 113444 },
{ url = "https://files.pythonhosted.org/packages/9f/7c/94f53b065a43f5dc1fbdd8b80fd8f41284315b543805c956619c0b8d92f0/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d", size = 101246 },
{ url = "https://files.pythonhosted.org/packages/62/5d/640360baac6ea6018ed5e34e6e80e33cfbae2aefde24f117587cd5efd4b7/wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f", size = 109320 },
{ url = "https://files.pythonhosted.org/packages/e3/cf/6c7a00ae86a2e9482c91170aefe93f4ccda06c1ac86c4de637c69133da59/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c", size = 110193 },
{ url = "https://files.pythonhosted.org/packages/cd/cc/aa718df0d20287e8f953ce0e2f70c0af0fba1d3c367db7ee8bdc46ea7003/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b", size = 100460 },
{ url = "https://files.pythonhosted.org/packages/f7/16/9f3ac99fe1f6caaa789d67b4e3c562898b532c250769f5255fa8b8b93983/wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab", size = 106347 },
{ url = "https://files.pythonhosted.org/packages/64/85/c77a331b2c06af49a687f8b926fc2d111047a51e6f0b0a4baa01ff3a673a/wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf", size = 37971 },
{ url = "https://files.pythonhosted.org/packages/05/9b/b2469f8be9efed24283fd7b9eeb8e913e9bc0715cf919ea8645e428ab7af/wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a", size = 40755 },
{ url = "https://files.pythonhosted.org/packages/4b/d9/a8ba5e9507a9af1917285d118388c5eb7a81834873f45df213a6fe923774/wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371", size = 23592 },
]
[[package]]
name = "yarl"
version = "1.15.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "multidict" },
{ name = "propcache" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/e1/d5427a061819c9f885f58bb0467d02a523f1aec19f9e5f9c82ce950d90d3/yarl-1.15.2.tar.gz", hash = "sha256:a39c36f4218a5bb668b4f06874d676d35a035ee668e6e7e3538835c703634b84", size = 169318 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/59/3ae125c97a2a8571ea16fdf59fcbd288bc169e0005d1af9946a90ea831d9/yarl-1.15.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9fcda20b2de7042cc35cf911702fa3d8311bd40055a14446c1e62403684afdc5", size = 136492 },
{ url = "https://files.pythonhosted.org/packages/f9/2b/efa58f36b582db45b94c15e87803b775eb8a4ca0db558121a272e67f3564/yarl-1.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0545de8c688fbbf3088f9e8b801157923be4bf8e7b03e97c2ecd4dfa39e48e0e", size = 88614 },
{ url = "https://files.pythonhosted.org/packages/82/69/eb73c0453a2ff53194df485dc7427d54e6cb8d1180fcef53251a8e24d069/yarl-1.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbda058a9a68bec347962595f50546a8a4a34fd7b0654a7b9697917dc2bf810d", size = 86607 },
{ url = "https://files.pythonhosted.org/packages/48/4e/89beaee3a4da0d1c6af1176d738cff415ff2ad3737785ee25382409fe3e3/yarl-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ac2bc069f4a458634c26b101c2341b18da85cb96afe0015990507efec2e417", size = 334077 },
{ url = "https://files.pythonhosted.org/packages/da/e8/8fcaa7552093f94c3f327783e2171da0eaa71db0c267510898a575066b0f/yarl-1.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd126498171f752dd85737ab1544329a4520c53eed3997f9b08aefbafb1cc53b", size = 347365 },
{ url = "https://files.pythonhosted.org/packages/be/fa/dc2002f82a89feab13a783d3e6b915a3a2e0e83314d9e3f6d845ee31bfcc/yarl-1.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3db817b4e95eb05c362e3b45dafe7144b18603e1211f4a5b36eb9522ecc62bcf", size = 344823 },
{ url = "https://files.pythonhosted.org/packages/ae/c8/c4a00fe7f2aa6970c2651df332a14c88f8baaedb2e32d6c3b8c8a003ea74/yarl-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:076b1ed2ac819933895b1a000904f62d615fe4533a5cf3e052ff9a1da560575c", size = 337132 },
{ url = "https://files.pythonhosted.org/packages/07/bf/84125f85f44bf2af03f3cf64e87214b42cd59dcc8a04960d610a9825f4d4/yarl-1.15.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f8cfd847e6b9ecf9f2f2531c8427035f291ec286c0a4944b0a9fce58c6446046", size = 326258 },
{ url = "https://files.pythonhosted.org/packages/00/19/73ad8122b2fa73fe22e32c24b82a6c053cf6c73e2f649b73f7ef97bee8d0/yarl-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:32b66be100ac5739065496c74c4b7f3015cef792c3174982809274d7e51b3e04", size = 336212 },
{ url = "https://files.pythonhosted.org/packages/39/1d/2fa4337d11f6587e9b7565f84eba549f2921494bc8b10bfe811079acaa70/yarl-1.15.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34a2d76a1984cac04ff8b1bfc939ec9dc0914821264d4a9c8fd0ed6aa8d4cfd2", size = 330397 },
{ url = "https://files.pythonhosted.org/packages/39/ab/dce75e06806bcb4305966471ead03ce639d8230f4f52c32bd614d820c044/yarl-1.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0afad2cd484908f472c8fe2e8ef499facee54a0a6978be0e0cff67b1254fd747", size = 334985 },
{ url = "https://files.pythonhosted.org/packages/c1/98/3f679149347a5e34c952bf8f71a387bc96b3488fae81399a49f8b1a01134/yarl-1.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c68e820879ff39992c7f148113b46efcd6ec765a4865581f2902b3c43a5f4bbb", size = 356033 },
{ url = "https://files.pythonhosted.org/packages/f7/8c/96546061c19852d0a4b1b07084a58c2e8911db6bcf7838972cff542e09fb/yarl-1.15.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:98f68df80ec6ca3015186b2677c208c096d646ef37bbf8b49764ab4a38183931", size = 357710 },
{ url = "https://files.pythonhosted.org/packages/01/45/ade6fb3daf689816ebaddb3175c962731edf300425c3254c559b6d0dcc27/yarl-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56ec1eacd0a5d35b8a29f468659c47f4fe61b2cab948ca756c39b7617f0aa5", size = 345532 },
{ url = "https://files.pythonhosted.org/packages/e7/d7/8de800d3aecda0e64c43e8fc844f7effc8731a6099fa0c055738a2247504/yarl-1.15.2-cp311-cp311-win32.whl", hash = "sha256:eedc3f247ee7b3808ea07205f3e7d7879bc19ad3e6222195cd5fbf9988853e4d", size = 78250 },
{ url = "https://files.pythonhosted.org/packages/3a/6c/69058bbcfb0164f221aa30e0cd1a250f6babb01221e27c95058c51c498ca/yarl-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:0ccaa1bc98751fbfcf53dc8dfdb90d96e98838010fc254180dd6707a6e8bb179", size = 84492 },
{ url = "https://files.pythonhosted.org/packages/e0/d1/17ff90e7e5b1a0b4ddad847f9ec6a214b87905e3a59d01bff9207ce2253b/yarl-1.15.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82d5161e8cb8f36ec778fd7ac4d740415d84030f5b9ef8fe4da54784a1f46c94", size = 136721 },
{ url = "https://files.pythonhosted.org/packages/44/50/a64ca0577aeb9507f4b672f9c833d46cf8f1e042ce2e80c11753b936457d/yarl-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa2bea05ff0a8fb4d8124498e00e02398f06d23cdadd0fe027d84a3f7afde31e", size = 88954 },
{ url = "https://files.pythonhosted.org/packages/c9/0a/a30d0b02046d4088c1fd32d85d025bd70ceb55f441213dee14d503694f41/yarl-1.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99e12d2bf587b44deb74e0d6170fec37adb489964dbca656ec41a7cd8f2ff178", size = 86692 },
{ url = "https://files.pythonhosted.org/packages/06/0b/7613decb8baa26cba840d7ea2074bd3c5e27684cbcb6d06e7840d6c5226c/yarl-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:243fbbbf003754fe41b5bdf10ce1e7f80bcc70732b5b54222c124d6b4c2ab31c", size = 325762 },
{ url = "https://files.pythonhosted.org/packages/97/f5/b8c389a58d1eb08f89341fc1bbcc23a0341f7372185a0a0704dbdadba53a/yarl-1.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:856b7f1a7b98a8c31823285786bd566cf06226ac4f38b3ef462f593c608a9bd6", size = 335037 },
{ url = "https://files.pythonhosted.org/packages/cb/f9/d89b93a7bb8b66e01bf722dcc6fec15e11946e649e71414fd532b05c4d5d/yarl-1.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:553dad9af802a9ad1a6525e7528152a015b85fb8dbf764ebfc755c695f488367", size = 334221 },
{ url = "https://files.pythonhosted.org/packages/10/77/1db077601998e0831a540a690dcb0f450c31f64c492e993e2eaadfbc7d31/yarl-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30c3ff305f6e06650a761c4393666f77384f1cc6c5c0251965d6bfa5fbc88f7f", size = 330167 },
{ url = "https://files.pythonhosted.org/packages/3b/c2/e5b7121662fd758656784fffcff2e411c593ec46dc9ec68e0859a2ffaee3/yarl-1.15.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:353665775be69bbfc6d54c8d134bfc533e332149faeddd631b0bc79df0897f46", size = 317472 },
{ url = "https://files.pythonhosted.org/packages/c6/f3/41e366c17e50782651b192ba06a71d53500cc351547816bf1928fb043c4f/yarl-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f4fe99ce44128c71233d0d72152db31ca119711dfc5f2c82385ad611d8d7f897", size = 330896 },
{ url = "https://files.pythonhosted.org/packages/79/a2/d72e501bc1e33e68a5a31f584fe4556ab71a50a27bfd607d023f097cc9bb/yarl-1.15.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9c1e3ff4b89cdd2e1a24c214f141e848b9e0451f08d7d4963cb4108d4d798f1f", size = 328787 },
{ url = "https://files.pythonhosted.org/packages/9d/ba/890f7e1ea17f3c247748548eee876528ceb939e44566fa7d53baee57e5aa/yarl-1.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:711bdfae4e699a6d4f371137cbe9e740dc958530cb920eb6f43ff9551e17cfbc", size = 332631 },
{ url = "https://files.pythonhosted.org/packages/48/c7/27b34206fd5dfe76b2caa08bf22f9212b2d665d5bb2df8a6dd3af498dcf4/yarl-1.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4388c72174868884f76affcdd3656544c426407e0043c89b684d22fb265e04a5", size = 344023 },
{ url = "https://files.pythonhosted.org/packages/88/e7/730b130f4f02bd8b00479baf9a57fdea1dc927436ed1d6ba08fa5c36c68e/yarl-1.15.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0e1844ad47c7bd5d6fa784f1d4accc5f4168b48999303a868fe0f8597bde715", size = 352290 },
{ url = "https://files.pythonhosted.org/packages/84/9b/e8dda28f91a0af67098cddd455e6b540d3f682dda4c0de224215a57dee4a/yarl-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5cafb02cf097a82d74403f7e0b6b9df3ffbfe8edf9415ea816314711764a27b", size = 343742 },
{ url = "https://files.pythonhosted.org/packages/66/47/b1c6bb85f2b66decbe189e27fcc956ab74670a068655df30ef9a2e15c379/yarl-1.15.2-cp312-cp312-win32.whl", hash = "sha256:156ececdf636143f508770bf8a3a0498de64da5abd890c7dbb42ca9e3b6c05b8", size = 78051 },
{ url = "https://files.pythonhosted.org/packages/7d/9e/1a897e5248ec53e96e9f15b3e6928efd5e75d322c6cf666f55c1c063e5c9/yarl-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:435aca062444a7f0c884861d2e3ea79883bd1cd19d0a381928b69ae1b85bc51d", size = 84313 },
{ url = "https://files.pythonhosted.org/packages/46/ab/be3229898d7eb1149e6ba7fe44f873cf054d275a00b326f2a858c9ff7175/yarl-1.15.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:416f2e3beaeae81e2f7a45dc711258be5bdc79c940a9a270b266c0bec038fb84", size = 135006 },
{ url = "https://files.pythonhosted.org/packages/10/10/b91c186b1b0e63951f80481b3e6879bb9f7179d471fe7c4440c9e900e2a3/yarl-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:173563f3696124372831007e3d4b9821746964a95968628f7075d9231ac6bb33", size = 88121 },
{ url = "https://files.pythonhosted.org/packages/bf/1d/4ceaccf836b9591abfde775e84249b847ac4c6c14ee2dd8d15b5b3cede44/yarl-1.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ce2e0f6123a60bd1a7f5ae3b2c49b240c12c132847f17aa990b841a417598a2", size = 85967 },
{ url = "https://files.pythonhosted.org/packages/93/bd/c924f22bdb2c5d0ca03a9e64ecc5e041aace138c2a91afff7e2f01edc3a1/yarl-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaea112aed589131f73d50d570a6864728bd7c0c66ef6c9154ed7b59f24da611", size = 325615 },
{ url = "https://files.pythonhosted.org/packages/59/a5/6226accd5c01cafd57af0d249c7cf9dd12569cd9c78fbd93e8198e7a9d84/yarl-1.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4ca3b9f370f218cc2a0309542cab8d0acdfd66667e7c37d04d617012485f904", size = 334945 },
{ url = "https://files.pythonhosted.org/packages/4c/c1/cc6ccdd2bcd0ff7291602d5831754595260f8d2754642dfd34fef1791059/yarl-1.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23ec1d3c31882b2a8a69c801ef58ebf7bae2553211ebbddf04235be275a38548", size = 336701 },
{ url = "https://files.pythonhosted.org/packages/ef/ff/39a767ee249444e4b26ea998a526838238f8994c8f274befc1f94dacfb43/yarl-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75119badf45f7183e10e348edff5a76a94dc19ba9287d94001ff05e81475967b", size = 330977 },
{ url = "https://files.pythonhosted.org/packages/dd/ba/b1fed73f9d39e3e7be8f6786be5a2ab4399c21504c9168c3cadf6e441c2e/yarl-1.15.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e6fdc976ec966b99e4daa3812fac0274cc28cd2b24b0d92462e2e5ef90d368", size = 317402 },
{ url = "https://files.pythonhosted.org/packages/82/e8/03e3ebb7f558374f29c04868b20ca484d7997f80a0a191490790a8c28058/yarl-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8657d3f37f781d987037f9cc20bbc8b40425fa14380c87da0cb8dfce7c92d0fb", size = 331776 },
{ url = "https://files.pythonhosted.org/packages/1f/83/90b0f4fd1ecf2602ba4ac50ad0bbc463122208f52dd13f152bbc0d8417dd/yarl-1.15.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:93bed8a8084544c6efe8856c362af08a23e959340c87a95687fdbe9c9f280c8b", size = 331585 },
{ url = "https://files.pythonhosted.org/packages/c7/f6/1ed7e7f270ae5f9f1174c1f8597b29658f552fee101c26de8b2eb4ca147a/yarl-1.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:69d5856d526802cbda768d3e6246cd0d77450fa2a4bc2ea0ea14f0d972c2894b", size = 336395 },
{ url = "https://files.pythonhosted.org/packages/e0/3a/4354ed8812909d9ec54a92716a53259b09e6b664209231f2ec5e75f4820d/yarl-1.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ccad2800dfdff34392448c4bf834be124f10a5bc102f254521d931c1c53c455a", size = 342810 },
{ url = "https://files.pythonhosted.org/packages/de/cc/39e55e16b1415a87f6d300064965d6cfb2ac8571e11339ccb7dada2444d9/yarl-1.15.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a880372e2e5dbb9258a4e8ff43f13888039abb9dd6d515f28611c54361bc5644", size = 351441 },
{ url = "https://files.pythonhosted.org/packages/fb/19/5cd4757079dc9d9f3de3e3831719b695f709a8ce029e70b33350c9d082a7/yarl-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c998d0558805860503bc3a595994895ca0f7835e00668dadc673bbf7f5fbfcbe", size = 345875 },
{ url = "https://files.pythonhosted.org/packages/83/a0/ef09b54634f73417f1ea4a746456a4372c1b044f07b26e16fa241bd2d94e/yarl-1.15.2-cp313-cp313-win32.whl", hash = "sha256:533a28754e7f7439f217550a497bb026c54072dbe16402b183fdbca2431935a9", size = 302609 },
{ url = "https://files.pythonhosted.org/packages/20/9f/f39c37c17929d3975da84c737b96b606b68c495cc4ee86408f10523a1635/yarl-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:5838f2b79dc8f96fdc44077c9e4e2e33d7089b10788464609df788eb97d03aad", size = 308252 },
{ url = "https://files.pythonhosted.org/packages/46/cf/a28c494decc9c8776b0d7b729c68d26fdafefcedd8d2eab5d9cd767376b2/yarl-1.15.2-py3-none-any.whl", hash = "sha256:0d3105efab7c5c091609abacad33afff33bdff0035bece164c98bcf5a85ef90a", size = 38891 },
]