Compare commits

...

348 Commits

Author SHA1 Message Date
Jedd Morgan 32dca397a3 Merge branch 'v3-dev' into jrm/speckle-client-https 2025-08-20 10:41:17 +01:00
Mucahit Bilal GOKER 5c19d9aa16 Merge pull request #300 from specklesystems/bilal/bump-specklepy-3.0.3
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
bump specklepy to 3.0.3
2025-07-24 21:21:09 +03:00
bimgeek 29d706e1b6 bump specklepy to 3.0.3 2025-07-24 21:18:14 +03:00
Mucahit Bilal GOKER 25d02673a7 Merge pull request #290 from specklesystems/bilal/cnx-2065-keep-track-of-loaded-object-properties
Persistent modifiers, visibility settings and tracking by applicationId
2025-07-22 17:19:53 +03:00
Jedd Morgan 6a4dc62622 small fix 2025-07-22 13:29:25 +01:00
Jedd Morgan bb9a5ea604 Merge branch 'v3-dev' into bilal/cnx-2065-keep-track-of-loaded-object-properties 2025-07-22 13:00:42 +01:00
Jedd Morgan 48759746dc fixed import 2025-07-22 11:57:56 +01:00
Jedd Morgan 4d5fb64893 Merge remote-tracking branch 'origin/v3-dev' into jrm/speckle-client-https 2025-07-22 11:50:27 +01:00
Jedd Morgan 6a3247aafa format 2025-07-22 11:49:32 +01:00
Jedd Morgan 2e995bd0fa chore(lint): Run Ruff (#298)
* ruff check

* format

* format
2025-07-22 11:48:25 +01:00
Jedd Morgan ae0280a630 format 2025-07-22 11:33:44 +01:00
Jedd Morgan 698b2a79fe Use ssl on client if only if url is https 2025-07-22 11:32:51 +01:00
Mucahit Bilal GOKER 38e6096ea9 Merge branch 'v3-dev' into bilal/cnx-2065-keep-track-of-loaded-object-properties 2025-07-22 12:43:49 +03:00
Mucahit Bilal GOKER 30e3398cd4 Merge pull request #296 from specklesystems/bilal/cnx-1798-update-bl_info
Update bl_info
2025-07-22 12:43:33 +03:00
bimgeek 3f7e98aff5 shorten description 2025-07-18 15:31:59 +03:00
Mucahit Bilal GOKER 1ad8429928 update bl_info and manifest 2025-07-18 15:24:36 +03:00
Mucahit Bilal GOKER a6c820183b fix: track collections by applicationId 2025-07-15 23:20:11 +03:00
Mucahit Bilal GOKER 56b6c813c0 remove logging of optimization metrics 2025-07-15 22:30:03 +03:00
Mucahit Bilal GOKER 8ab110f7ec only store modified objects and collections 2025-07-15 22:23:44 +03:00
Mucahit Bilal GOKER 11ff018f18 refactor visibility settings and modifiers storage: model cards > temp storage 2025-07-15 22:09:26 +03:00
Mucahit Bilal GOKER 227f63d266 Merge pull request #288 from specklesystems/bilal/cnx-2003-cache-speckle-clients
Bilal/cnx 2003 cache speckle clients
2025-07-15 16:04:58 +03:00
Mucahit Bilal GOKER 9e8aaf4f3b Merge branch 'v3-dev' into bilal/cnx-2003-cache-speckle-clients 2025-07-15 16:04:28 +03:00
Mucahit Bilal GOKER afcb760bbf added typing 2025-07-14 13:42:54 +03:00
Mucahit Bilal GOKER 58283439ab comments cleanup 2025-07-11 15:19:43 +03:00
Mucahit Bilal GOKER 0c29a2ec0a replace selecting by name logic to applicationId 2025-07-11 12:50:31 +03:00
Mucahit Bilal GOKER 4ec62d4168 Revert "rename speckle_application_id to applicationId"
This reverts commit 8d596823ed.
2025-07-11 12:09:15 +03:00
Mucahit Bilal GOKER 8d596823ed rename speckle_application_id to applicationId 2025-07-11 12:06:47 +03:00
Mucahit Bilal GOKER ccd62e3452 remove 8 char limit from ids 2025-07-11 11:45:41 +03:00
Mucahit Bilal GOKER 1bd08497e6 Merge remote-tracking branch 'origin/v3-dev' into bilal/cnx-2065-keep-track-of-loaded-object-properties 2025-07-11 11:38:18 +03:00
Jedd Morgan d23cc5a738 Merge pull request #289 from specklesystems/jrm/requirementstxt
Ensure hashes are included in requirements.txt creaiton
2025-07-10 15:59:24 +01:00
Mucahit Bilal GOKER 3e2ac4b5b6 preserve modifiers 2025-07-06 22:14:28 +03:00
Mucahit Bilal GOKER 928bc15ff1 preserve layer collection visibility settings 2025-07-05 21:05:19 +03:00
Mucahit Bilal GOKER e410e40060 preserve object visibility settings and update object removal function 2025-07-05 19:54:39 +03:00
Mucahit Bilal GOKER d1f2c938b1 clear cache on unregister 2025-06-25 22:22:46 +03:00
Mucahit Bilal GOKER 388ec2bdfd use cached client for managers 2025-06-25 21:45:10 +03:00
Mucahit Bilal GOKER b057c6c0da use cached client for publish and load 2025-06-25 21:44:29 +03:00
Mucahit Bilal GOKER 40089bdbb8 resolve get_active_workspace 2025-06-25 21:44:10 +03:00
Mucahit Bilal GOKER 49dd688219 create project and model - use cached client 2025-06-25 21:43:51 +03:00
Mucahit Bilal GOKER 6993e8cb83 initialize client on startup 2025-06-25 21:41:54 +03:00
Mucahit Bilal GOKER 709015b9d8 Merge branch 'v3-dev' into bilal/cnx-2003-cache-speckle-clients 2025-06-25 16:18:07 +03:00
Dogukan Karatas c5e0dfa36b Merge pull request #287 from specklesystems/bilal/handle-no-account-state
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
Bilal/handle no account state
2025-06-23 11:54:04 +02:00
Mucahit Bilal GOKER 1f72741b62 update collapse icon 2025-06-23 11:34:43 +03:00
Mucahit Bilal GOKER 0f8f7e02be Added error handling in get_active_workspace to return default workspace if no account is found for the given ID. 2025-06-23 11:14:09 +03:00
Dogukan Karatas f3e188b4f8 Merge pull request #286 from specklesystems/bilal/cnx-2013-implement-workspace-dropdown
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
Bilal/cnx 2013 implement workspace dropdown
2025-06-17 15:28:12 +02:00
Mucahit Bilal GOKER 346b0210a5 resolve leftover conflicts 2025-06-17 16:00:36 +03:00
Mucahit Bilal GOKER 5d072bbb1d Merge branch 'v3-dev' into bilal/cnx-2013-implement-workspace-dropdown 2025-06-17 15:55:11 +03:00
Mucahit Bilal GOKER fcc8527c9f Merge pull request #285 from specklesystems/bilal/account-selection-button
Bilal/cnx 2012 account selection button
2025-06-17 14:57:15 +03:00
Mucahit Bilal GOKER 2814dabe14 Merge pull request #284 from specklesystems/bilal/fix-model-card-publish-load-buttons
Bilal/cnx 1994 fix model card publish load buttons
2025-06-17 14:49:13 +03:00
Mucahit Bilal GOKER 0f6150b272 hide selection filter dropdown 2025-06-17 14:47:35 +03:00
Mucahit Bilal GOKER e3bc770369 remove unnecessary checks 2025-06-17 14:42:18 +03:00
Mucahit Bilal GOKER 1bb6ce7b63 merge workspace properties into one 2025-06-17 12:18:46 +03:00
Mucahit Bilal GOKER 6bf05f6ffd bind dialog to button in project selection dialog 2025-06-17 11:59:54 +03:00
Mucahit Bilal GOKER 684c868cd0 add selected workspace to init 2025-06-17 11:36:20 +03:00
Mucahit Bilal GOKER 27009a61e9 workspace selection dialog ui 2025-06-17 11:33:52 +03:00
Mucahit Bilal GOKER 27ff32f584 remove duplicate Windowmanager property invokes 2025-06-17 10:30:25 +03:00
Mucahit Bilal GOKER 223ede3ec7 show sign in button when no accounts present 2025-06-17 08:14:38 +03:00
Mucahit Bilal GOKER 3b2e609888 print to self report 2025-06-17 08:02:11 +03:00
Mucahit Bilal GOKER f4caa55298 cleanup + store selected account 2025-06-17 07:51:17 +03:00
Mucahit Bilal GOKER 166d686b9d update workspace and projects list after execute 2025-06-17 07:50:57 +03:00
Mucahit Bilal GOKER f3f43eebd5 add button to project selection dialog 2025-06-17 07:25:51 +03:00
Mucahit Bilal GOKER 3c16b35232 first pass 2025-06-17 06:57:01 +03:00
Mucahit Bilal GOKER 86fb480a15 draw function shouldn't return anything 2025-06-16 14:10:34 +03:00
Mucahit Bilal GOKER abef8f85d9 update misleading descriptions 2025-06-16 14:10:27 +03:00
Mucahit Bilal GOKER 33a7009585 add type hints for model_card_objects 2025-06-16 14:10:22 +03:00
Mucahit Bilal GOKER 0249ecb313 remove double registry 2025-06-16 14:10:11 +03:00
Mucahit Bilal GOKER bbf8a3b45e remove unused time import 2025-06-16 14:09:52 +03:00
Mucahit Bilal GOKER f1eec55633 remove time limits 2025-06-16 14:09:48 +03:00
Mucahit Bilal GOKER f2bc9a9701 client caching first pass 2025-06-16 14:09:42 +03:00
Mucahit Bilal GOKER 1eb662c6a7 add ui mode to model card id 2025-06-16 14:03:53 +03:00
Mucahit Bilal GOKER c08d4c398f remove unnecessary json usage for saving model cards 2025-06-15 00:52:00 +03:00
Mucahit Bilal GOKER a81a44ca1d remove unused search input 2025-06-15 00:26:05 +03:00
Mucahit Bilal GOKER c4c4431ed2 move refresh after load 2025-06-15 00:22:57 +03:00
Mucahit Bilal GOKER fd23d40a5c force refresh after loading a new version 2025-06-15 00:22:12 +03:00
Mucahit Bilal GOKER f2d6e03ad8 gracefully handle missing icon files 2025-06-15 00:20:54 +03:00
Mucahit Bilal GOKER d5c9097afa fix model card state when publish fails 2025-06-15 00:18:43 +03:00
Mucahit Bilal GOKER 90cd0706ba fix return type mismatch on load operation 2025-06-15 00:16:08 +03:00
Mucahit Bilal GOKER 2daf0de073 remove report from panel 2025-06-15 00:14:54 +03:00
Mucahit Bilal GOKER 3653ece109 removed unnecessary registrations 2025-06-15 00:13:45 +03:00
Mucahit Bilal GOKER c29e0bf8ee duplicate registration of speckle_project 2025-06-15 00:11:37 +03:00
Mucahit Bilal GOKER 95a84eda53 clear model_card_id for selection and version buttons in main panel 2025-06-14 21:41:22 +03:00
Mucahit Bilal GOKER 8116402023 add message to load button in model card 2025-06-13 23:55:03 +03:00
Mucahit Bilal GOKER f6238eb29c fix selection button text showing previous selection 2025-06-13 23:37:55 +03:00
Mucahit Bilal GOKER 0a4c9ec380 update select objects buttons icon 2025-06-13 23:31:14 +03:00
Mucahit Bilal GOKER 4b8b1393ba update descriptions 2025-06-13 23:31:02 +03:00
Mucahit Bilal GOKER ee01b28645 update object highlight button 2025-06-13 19:54:50 +03:00
Mucahit Bilal GOKER 06225a8aab update speckle logo 2025-06-13 19:43:59 +03:00
Mucahit Bilal GOKER 1129d169cb fix duplicate collection 2025-06-13 17:09:20 +03:00
Mucahit Bilal GOKER a787bfb8a3 nil safety 2025-06-13 16:36:12 +03:00
Mucahit Bilal GOKER 700ec92ed2 first pass - update loaded version from model card 2025-06-13 16:22:24 +03:00
Mucahit Bilal GOKER 7fbf0a827b fix version selection dialog and remove search 2025-06-13 15:12:39 +03:00
Mucahit Bilal GOKER b6f659e91f cleanup if else in main panel model cards 2025-06-13 14:53:33 +03:00
Mucahit Bilal GOKER e9628cde77 use version message on selection filter dialog 2025-06-13 14:42:58 +03:00
Mucahit Bilal GOKER cd919353f7 remove unused imports 2025-06-13 09:24:22 +03:00
Mucahit Bilal GOKER 6400138a3c publish after selection 2025-06-13 09:24:11 +03:00
Mucahit Bilal GOKER 9b8ddcc706 remove collection name from model card 2025-06-13 08:33:59 +03:00
Mucahit Bilal GOKER ba3e0b275e fix collection get 2025-06-13 08:06:41 +03:00
Mucahit Bilal GOKER 45a68b3c5f update model card selection 2025-06-13 07:54:20 +03:00
Mucahit Bilal GOKER 49379a3e33 create property groups file 2025-06-13 07:43:36 +03:00
Mucahit Bilal GOKER b30ffdc7d1 remove comments 2025-06-13 07:02:41 +03:00
Mucahit Bilal GOKER 2e53a2fea4 check if model card exists - publish 2025-06-13 06:59:28 +03:00
Mucahit Bilal GOKER 9dc51ee1dc update model card objects to accept a list of object names 2025-06-13 06:58:59 +03:00
Mucahit Bilal GOKER 58e7ac5138 Merge remote-tracking branch 'origin/bilal/fix-model-card-publish-load-buttons' into bilal/fix-model-card-publish-load-buttons 2025-06-13 06:33:26 +03:00
Mucahit Bilal GOKER 6bc9bfdc28 check if model card exists - load 2025-06-13 06:33:15 +03:00
Mucahit Bilal GOKER cce1f3b092 check if model card exists 2025-06-13 06:07:50 +03:00
Mucahit Bilal GOKER ebcbe29398 model card utils 2025-06-13 06:04:48 +03:00
Mucahit Bilal GOKER 59b6b21736 rename load latest -> load model card 2025-06-13 06:00:43 +03:00
Mucahit Bilal GOKER 6c9823776a safety checks on publish 2025-06-13 05:34:35 +03:00
Mucahit Bilal GOKER c77e7642b7 first pass - model card publish button 2025-06-12 22:09:48 +03:00
Mucahit Bilal GOKER 9240fce897 Merge remote-tracking branch 'origin/bilal/fix-model-card-publish-load-buttons' into bilal/fix-model-card-publish-load-buttons 2025-06-12 20:02:51 +03:00
Mucahit Bilal GOKER a87b7a830e first pass - load button on model card 2025-06-12 20:02:45 +03:00
Mucahit Bilal GOKER aad7943f1a first pass 2025-06-12 19:55:31 +03:00
Mucahit Bilal GOKER 589b309d91 moved select objects to a separate function 2025-06-12 14:57:40 +03:00
Mucahit Bilal GOKER 95cb7c35de Merge branch 'v3-dev' into bilal/fix-model-card-publish-load-buttons 2025-06-12 14:33:17 +03:00
Mucahit Bilal GOKER aab3f1792a Merge pull request #281 from specklesystems/dogukan/cnx-1930-blender-receive-blocks-as-linked-duplicates
feat: option to load instances as linked duplicates
2025-06-12 14:32:50 +03:00
Mucahit Bilal GOKER 80a6e424e4 Merge pull request #282 from specklesystems/bilal/select-objects-by-name
Bilal/select objects by name
2025-06-12 14:32:36 +03:00
Mucahit Bilal GOKER f08cac36e7 Merge pull request #283 from specklesystems/bilal/fix-no-versions-in-model
fix adding model urls with no versions
2025-06-12 14:32:25 +03:00
Mucahit Bilal GOKER aa01b5ccf4 remove columns 2025-06-12 14:25:48 +03:00
Mucahit Bilal GOKER da7d260542 rename load_latest operator to model_card_load_button 2025-06-12 13:58:57 +03:00
Dogukan Karatas 64e0f65e29 fixed transform 2025-06-12 12:57:22 +02:00
Dogukan Karatas fedf036e2f fixed nested structures 2025-06-12 12:40:08 +02:00
Mucahit Bilal GOKER b9df9b09de fix adding model urls with no versions 2025-06-12 12:14:03 +03:00
Mucahit Bilal GOKER 3afdb916b3 select only objects in the active view layer 2025-06-11 23:38:38 +03:00
Mucahit Bilal GOKER 8d7b0adea4 better error handling 2025-06-11 22:31:22 +03:00
Mucahit Bilal GOKER 7a6d956ccf fix saving model card issue 2025-06-11 22:31:12 +03:00
Mucahit Bilal GOKER d272743642 select published elements 2025-06-11 22:04:21 +03:00
Mucahit Bilal GOKER c3ce77fe4d first pass 2025-06-11 22:00:54 +03:00
Dogukan Karatas 564aa58ebf Merge pull request #280 from specklesystems/dogukan/apply-modifiers
feat: apply modifiers option
2025-06-11 20:42:52 +02:00
Dogukan Karatas 48fe756fcd converts instances as linked duplicates 2025-06-11 20:39:59 +02:00
Dogukan Karatas 8e2f546cd6 adds applying modififers 2025-06-11 20:08:28 +02:00
Dogukan Karatas 3219172fed Merge pull request #276 from specklesystems/dogukan/query-optimizations
feat: projects with permission in a single query
2025-06-11 15:21:09 +02:00
Mucahit Bilal GOKER 098d576a0b redraw after selection
Ensures the UI updates immediately after adding an object
to the selection filter dialog. This provides more responsive feedback to the user.
2025-06-11 16:13:53 +03:00
Mucahit Bilal GOKER 1950c412c2 fail when trying to create a model in personal workspace 2025-06-11 16:08:27 +03:00
Dogukan Karatas b646b2c7b1 Merge branch 'v3-dev' into dogukan/query-optimizations 2025-06-11 14:44:22 +02:00
Dogukan Karatas 19f22296b4 Merge pull request #279 from specklesystems/bilal/cnx-1980-add-version-message-to-blender
set version message
2025-06-11 14:44:00 +02:00
Dogukan Karatas 240fb6cbbe Merge branch 'v3-dev' into bilal/cnx-1980-add-version-message-to-blender 2025-06-11 14:43:39 +02:00
Dogukan Karatas 5ca6a3f2c2 Merge pull request #277 from specklesystems/bilal/create_project_button
Bilal/create project button
2025-06-11 14:43:08 +02:00
Dogukan Karatas 06fef6ca91 Merge branch 'v3-dev' into bilal/create_project_button 2025-06-11 14:40:49 +02:00
Dogukan Karatas dcbe2d7814 Merge pull request #278 from specklesystems/bilal/create_model_button
Bilal/create model button
2025-06-11 14:39:32 +02:00
Dogukan Karatas b66850b994 adds personal projects 2025-06-11 13:41:50 +02:00
Mucahit Bilal GOKER 6a337ccdad default empty string 2025-06-11 14:20:43 +03:00
Mucahit Bilal GOKER 92fd5387ef set version message 2025-06-11 14:14:59 +03:00
Dogukan Karatas 6205afdda5 optimization on workspace id 2025-06-11 13:10:33 +02:00
Dogukan Karatas d060c3b8cf filter updated 2025-06-11 12:57:31 +02:00
Dogukan Karatas 5dd3dd713a search added 2025-06-11 12:40:34 +02:00
Mucahit Bilal GOKER 4fc1053af7 error handling 2025-06-10 23:38:25 +03:00
Mucahit Bilal GOKER 80204faedf first pass 2025-06-10 23:30:52 +03:00
Mucahit Bilal GOKER 06a47a2f9b exception handling 2025-06-10 22:27:34 +03:00
Mucahit Bilal GOKER b65a7a3d4a unused wm 2025-06-10 22:27:16 +03:00
Mucahit Bilal GOKER 7064574387 unused workspace name 2025-06-10 22:27:00 +03:00
Mucahit Bilal GOKER 169ae4d32a first pass 2025-06-10 22:07:36 +03:00
Dogukan Karatas 4d44b41e2f optimized query 2025-06-10 16:29:18 +02:00
Dogukan Karatas a1f835dc77 Merge pull request #272 from specklesystems/dogukan/cnx-1976-conversion-of-collections-in-blender-send
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
feat: collection conversion
2025-06-06 10:59:08 +02:00
Dogukan Karatas e2172216a5 Merge pull request #275 from specklesystems/jrm/uv-installer
chore(installer): Revert uv implementation of the installer
2025-06-06 10:58:53 +02:00
Jedd Morgan 965c3e9c6e Removed uv from installer 2025-06-05 14:16:09 +01:00
Dogukan Karatas 65e4812ba1 fixes the object selection 2025-06-05 15:16:01 +02:00
Jedd Morgan 87df86f723 Format 2025-06-05 14:15:53 +01:00
Dogukan Karatas fd32371be3 adds ancestor collections 2025-06-05 12:56:26 +02:00
Dogukan Karatas 19c1334bb3 adds collection conversion 2025-06-04 16:38:34 +02:00
Dogukan Karatas 7a36450143 Merge pull request #271 from specklesystems/dogukan/fix-version-import
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: global import of bl_info
2025-06-04 10:10:44 +02:00
Dogukan Karatas d37fce644b adds global import 2025-06-04 10:07:41 +02:00
Dogukan Karatas 00bcefba56 Merge pull request #269 from specklesystems/dogukan/fixing-applicationIds
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: unique applicationIds
2025-06-03 18:17:58 +02:00
Dogukan Karatas d53bc064e1 removes the volume calculation 2025-06-03 17:58:11 +02:00
Dogukan Karatas 50affbedf1 sets unique ids 2025-06-03 17:55:09 +02:00
Dogukan Karatas f00aeecead Merge pull request #268 from specklesystems/bilal/send-ui
feat: publish blender models
2025-06-03 17:52:08 +02:00
Dogukan Karatas bbf2ee79be fixes the applicationId 2025-06-03 16:22:11 +02:00
Dogukan Karatas abb1f042d4 Merge pull request #262 from specklesystems/bilal/v3-cnx-1815-blender-connector-legacy-to-next-gen-transition
v3 - prep for next-gen as stable
2025-06-02 14:42:51 +02:00
Dogukan Karatas 04b4e02d05 adds workspace to metrics 2025-06-02 13:02:52 +02:00
Dogukan Karatas 144990db7a adds metrics 2025-06-02 12:35:55 +02:00
Mucahit Bilal GOKER a5592cdd7d Merge branch 'bilal/cnx-1880-implement-send-ui' into bilal/send-ui 2025-05-28 22:44:02 +03:00
Dogukan Karatas 249e3a0a84 fixed model card issue 2025-05-28 20:54:05 +02:00
Dogukan Karatas f869139d2a fix the specklepy version 2025-05-28 19:41:13 +02:00
Dogukan Karatas 3954369db5 Merge branch 'v3-dev' into bilal/send-ui 2025-05-28 19:39:58 +02:00
Dogukan Karatas 7165d99b76 splitting the edges 2025-05-28 15:11:13 +02:00
Dogukan Karatas 8234db872d fixed the displayValue hierarchy 2025-05-27 22:56:14 +02:00
Dogukan Karatas 9acc84387f updates units 2025-05-27 21:47:06 +02:00
Dogukan Karatas 4eb1f2b773 base collection added 2025-05-27 21:04:27 +02:00
Dogukan Karatas 4d60607a92 materials are updated 2025-05-27 20:53:24 +02:00
Dogukan Karatas 09f809fc8f adds render material conversion 2025-05-27 15:34:26 +02:00
Mucahit Bilal GOKER 9f6833c36e show publish button only after selection made 2025-05-24 22:14:29 +03:00
Mucahit Bilal GOKER 1cc8ed804a store user selection in wm 2025-05-24 22:12:39 +03:00
Mucahit Bilal GOKER eb0f036641 publish > selection filter button 2025-05-24 22:09:11 +03:00
Mucahit Bilal GOKER f140c41af6 replace invoke on publish button 2025-05-24 21:59:42 +03:00
Mucahit Bilal GOKER 8a91634f55 placeholder publish button 2025-05-24 21:58:37 +03:00
Mucahit Bilal GOKER b025819307 hide version selector and load button when publish is selected 2025-05-24 21:57:40 +03:00
Mucahit Bilal GOKER 3ec98d3dd2 remove ui mode from speckle state 2025-05-24 21:55:07 +03:00
Mucahit Bilal GOKER ba182d48c4 add ui mode radio button 2025-05-24 21:53:54 +03:00
Dogukan Karatas f35853cff1 adds mesh to speckle 2025-05-21 15:59:03 +02:00
Dogukan Karatas d64ad50d32 adds spline conversion 2025-05-21 12:57:51 +02:00
Dogukan Karatas ab0b012f58 first pass of conversions 2025-05-20 16:51:53 +02:00
Dogukan Karatas dfa8ff2929 Merge pull request #263 from specklesystems/bilal/cnx-1770-set-viewport-display-color-in-addition-to-material-color
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
Set Viewport Display color of materials
2025-05-15 15:53:17 +02:00
Mucahit Bilal GOKER 14b628cd3d Merge pull request #265 from specklesystems/dogukan/cnx-1814-project-visibility-workspace-for-blender
fix: updates the sdk version
2025-05-15 16:34:09 +03:00
Dogukan Karatas dfc1d4aaf7 updates the sdk version 2025-05-15 15:06:07 +02:00
Mucahit Bilal GOKER 8c39da370f update tagline 2025-05-15 14:22:43 +03:00
Mucahit Bilal GOKER 532eba37a5 remove beta tag 2025-05-15 14:18:29 +03:00
Mucahit Bilal GOKER 39207e5716 update website 2025-05-15 14:17:59 +03:00
Mucahit Bilal GOKER 48ee5c1ffe final touch 2025-05-15 12:51:23 +03:00
Mucahit Bilal GOKER b41eb812ab use opacity too 2025-05-15 12:47:40 +03:00
Mucahit Bilal GOKER fece1a0151 diffuse color as viewport display color 2025-05-15 07:21:45 +03:00
Dogukan Karatas 3a9e03b982 Merge pull request #260 from specklesystems/dogukan/update-readme
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
update readme for v3-dev
2025-05-13 11:59:28 +02:00
Dogukan Karatas 96f0db5f63 updates the version 2025-05-13 11:54:19 +02:00
Dogukan Karatas 56a43bca06 small update 2025-05-13 11:42:15 +02:00
Dogukan Karatas 5f451f0143 updates readme 2025-05-13 11:41:25 +02:00
Dogukan Karatas 4cda3ff143 Merge pull request #254 from specklesystems/dogukan/update-project-permissions
feat: server-side checks for project permission
2025-05-13 11:28:31 +02:00
Dogukan Karatas 167a691294 Merge pull request #257 from specklesystems/dogukan/cnx-1775-archicad-to-blender-objects-not-named-with-the-name
fix: naming of Archicad objects
2025-05-13 11:28:07 +02:00
Dogukan Karatas 2f320d360e solving stashes 2025-05-13 11:26:38 +02:00
Dogukan Karatas 25ebdb8c3b solving merge conflict 2025-05-12 17:41:24 +02:00
Dogukan Karatas afd52e788f Merge branch 'v3-dev' into dogukan/cnx-1775-archicad-to-blender-objects-not-named-with-the-name 2025-05-12 17:40:25 +02:00
Dogukan Karatas 7997eedb05 updated uv lock 2025-05-12 17:35:03 +02:00
Dogukan Karatas e98aecaa82 Merge branch 'v3-dev' into dogukan/update-project-permissions 2025-05-12 17:32:47 +02:00
Dogukan Karatas 73e44c6b0e instance check for dataobject 2025-05-12 17:00:06 +02:00
Dogukan Karatas c2b7752539 updates the naming of dataobjects 2025-05-12 11:51:58 +02:00
Mucahit Bilal GOKER 050fc61377 Merge pull request #253 from specklesystems/jedd/cnx-1114-add-support-for-vertex-normals
feat(receive): Added support for receiving vertex normals from Rhino
2025-05-12 11:16:24 +03:00
Mucahit Bilal GOKER 3ab4774dc8 Merge pull request #259 from specklesystems/bilal/cnx-1778-sign-in-button-when-no-accounts-available
Bilal/cnx 1778 sign in button when no accounts available
2025-05-12 11:13:03 +03:00
Mucahit Bilal GOKER d9727005cf don't show workspaces or projects when no accounts 2025-05-11 21:52:38 +03:00
Mucahit Bilal GOKER f73fc604d0 no account no workspaces 2025-05-11 21:50:08 +03:00
Mucahit Bilal GOKER 4bd3dcaf61 fix: workspaces error when no accounts found 2025-05-11 21:49:39 +03:00
Mucahit Bilal GOKER a2ecf1d20e Merge pull request #256 from specklesystems/dogukan/cnx-1774-loading-sketchup-models-in-blender
fix: unit conversion for instances
2025-05-09 17:49:21 +03:00
Mucahit Bilal GOKER 74228b13dd Merge pull request #258 from specklesystems/dogukan/fix-polycurve-splines
fix: adds properties to polycurves
2025-05-09 17:44:54 +03:00
Dogukan Karatas 5aa39fefa4 adds properties to polycuve 2025-05-09 16:40:22 +02:00
Dogukan Karatas 0205636b28 update naming of dataobjects 2025-05-09 16:22:21 +02:00
Mucahit Bilal GOKER 9869ed3b63 Merge pull request #255 from specklesystems/dogukan/cnx-1764-blender-curves-with-arcs-and-closed-splines
fix: better arc and curve conversion
2025-05-09 17:17:14 +03:00
Dogukan Karatas 6a29e0c7cd updates unit conversion 2025-05-09 15:48:35 +02:00
Dogukan Karatas 011e1b90c1 fromat 2025-05-09 15:03:53 +02:00
Dogukan Karatas a1189e3878 updates polycurves 2025-05-09 14:52:36 +02:00
Dogukan Karatas 4fb801b44c updates curve conversion 2025-05-09 14:12:19 +02:00
Dogukan Karatas 80eeb2de39 updates arc and curve conversion 2025-05-09 13:51:04 +02:00
Dogukan Karatas 753760fee4 simplifies the authorization 2025-05-08 13:52:46 +02:00
Dogukan Karatas 8fbb01dee4 updates project permissions 2025-05-08 13:35:45 +02:00
Jedd Morgan 41f2b93347 Fix backwards compatibility with meshes pre-normals 2025-05-08 14:13:57 +03:00
Jedd Morgan 98a6f3251d Merge branch 'v3-dev' into jedd/cnx-1114-add-support-for-vertex-normals 2025-05-08 13:54:36 +03:00
Jedd Morgan 8287ed2e57 Added vertex normals 2025-05-08 13:51:32 +03:00
Mucahit Bilal GOKER 3f5cf49971 Merge pull request #252 from specklesystems/dogukan/implicit-access
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
fix: checks for permissions
2025-05-06 10:45:52 +03:00
Dogukan Karatas efc2c0b07c seperation of ui and sdk 2025-05-06 09:38:00 +02:00
Dogukan Karatas 242625ebe3 checks permissons 2025-05-05 15:19:19 +02:00
Dogukan Karatas 5f5af2b15a disables the projects with no access 2025-05-05 13:35:34 +02:00
Jedd Morgan ff33b82013 First pass adding vertex normal receive 2025-05-02 10:43:16 +03:00
Dogukan Karatas dbe059f507 Merge pull request #251 from specklesystems/dogukan/material-consistency
feat: material consistency
2025-05-01 22:06:57 +02:00
Dogukan Karatas 9d19e52037 adds a material mapping 2025-05-01 21:35:32 +02:00
Dogukan Karatas f0495ab093 Merge pull request #249 from specklesystems/dogukan/get-version-message
fix: none check for version messages
2025-05-01 17:14:40 +02:00
Dogukan Karatas e57dacc6ef updates the version check 2025-05-01 17:11:00 +02:00
Dogukan Karatas 69dbf2f117 updates version manager 2025-05-01 17:00:52 +02:00
Dogukan Karatas 3cf3867877 Merge pull request #247 from specklesystems/bilal/cnx-1650-integrating-workspaces-to-blender-dui
Bilal/cnx 1650 integrating workspaces to blender dui
2025-05-01 11:49:58 +02:00
Dogukan Karatas 3577f3fd6a updates personal account check 2025-05-01 10:58:32 +02:00
Mucahit Bilal GOKER 242af16e81 Merge remote-tracking branch 'origin/bilal/cnx-1650-integrating-workspaces-to-blender-dui' into bilal/cnx-1650-integrating-workspaces-to-blender-dui 2025-05-01 11:19:05 +03:00
Dogukan Karatas 20f36ffe19 Merge pull request #248 from specklesystems/dogukan/cnx-1476-instance-proxies
feat: conversion of instance proxies
2025-05-01 10:17:33 +02:00
Mucahit Bilal GOKER b103d0da09 only show completed workspaces 2025-05-01 11:16:58 +03:00
Mucahit Bilal GOKER 4add9c5d72 bump specklepy version 2025-05-01 11:16:44 +03:00
Mucahit Bilal GOKER aecb15e549 bump specklepy version 2025-04-30 19:56:21 +03:00
Dogukan Karatas bff8140559 cleaning up some checks 2025-04-30 18:44:12 +02:00
Dogukan Karatas fe98f71be2 fixes the scaling issue 2025-04-30 18:03:51 +02:00
Mucahit Bilal GOKER f2008502f3 support self hosters 2025-04-30 18:24:53 +03:00
Dogukan Karatas 2981c1270b fixes scaling weirdly 2025-04-30 17:22:57 +02:00
Mucahit Bilal GOKER 3011921df9 only list valid versions 2025-04-30 17:29:07 +03:00
Mucahit Bilal GOKER 79ecba213a strip non ascii from model selection dialog texts 2025-04-30 17:28:54 +03:00
Mucahit Bilal GOKER 06db6653fb swıtch the order of account text > move server url to the end so it's visible 2025-04-30 17:28:29 +03:00
Mucahit Bilal GOKER 86200091dc strip non ascii chars 2025-04-30 14:45:19 +03:00
Mucahit Bilal GOKER 4b2a678605 update workspaces when account changes 2025-04-30 14:45:09 +03:00
Dogukan Karatas 1ca2c03ec0 check attributes 2025-04-30 13:07:50 +02:00
Mucahit Bilal GOKER 528b81d294 fetch personal projects 2025-04-30 13:46:05 +03:00
Dogukan Karatas d0dd57a731 sorts nested proxies 2025-04-30 11:56:15 +02:00
Mucahit Bilal GOKER 25258fd39f update projects list when workspace changes 2025-04-30 07:29:15 +03:00
Mucahit Bilal GOKER a884f7a6ea filter projects by workspace 2025-04-30 07:24:05 +03:00
Mucahit Bilal GOKER 6e0df0d4e4 added personal projects to workspaces dropdown 2025-04-30 07:15:51 +03:00
Mucahit Bilal GOKER 58ff3e667e initial workspace dropdown 2025-04-30 07:05:50 +03:00
Mucahit Bilal GOKER 3f5c933ee9 Merge branch 'bilal/cnx-1690-get_accounts-is-continously-called-in-the-background' into bilal/cnx-1650-integrating-workspaces-to-blender-dui 2025-04-30 06:17:56 +03:00
Mucahit Bilal GOKER d137c7b991 store accounts in window manager and initialize in invoke 2025-04-29 23:30:18 +03:00
Mucahit Bilal GOKER 5eec02296b move print statement out of list 2025-04-29 21:59:42 +03:00
Mucahit Bilal GOKER 681acd81c8 added logging to update projects function 2025-04-29 18:56:55 +03:00
Dogukan Karatas 09dd819504 some cleanup 2025-04-29 17:32:24 +02:00
Dogukan Karatas 211296c803 unlink instance collection from scene 2025-04-29 17:29:04 +02:00
Mucahit Bilal GOKER 0fd5448342 print account manager 2025-04-29 17:53:50 +03:00
Dogukan Karatas 3002d7a31b adds poc for instance proxy conversion 2025-04-29 16:36:38 +02:00
Mucahit Bilal GOKER 8eee1ede58 specklepy version update 2025-04-29 17:24:23 +03:00
Mucahit Bilal GOKER 75763d0929 show publish button only after selection made 2025-04-25 11:28:26 +03:00
Mucahit Bilal GOKER 2f2a67a569 store user selection in wm 2025-04-25 11:28:14 +03:00
Mucahit Bilal GOKER 7246d239be publish > selection filter button 2025-04-24 23:06:12 +03:00
Mucahit Bilal GOKER 4d1fe83c1e show placeholder publish button 2025-04-24 22:52:30 +03:00
Mucahit Bilal GOKER 1097bba539 hide version selector and load button when publish is selected 2025-04-24 22:48:43 +03:00
Mucahit Bilal GOKER eb25b6d821 remove ui mode from speckle state 2025-04-24 22:46:26 +03:00
Mucahit Bilal GOKER 80515fdc69 add ui mode radio button in the main panel 2025-04-24 22:46:14 +03:00
Jedd Morgan 4a340ef1ae refactor(ci): Update workflow to use new consolidated deployment workflow (#238)
Release workflow / Build Zip (push) Has been cancelled
Release workflow / deploy-installers (push) Has been cancelled
* Update release workflow

* new workflow

* fileversion experiment

* Always use the run number in the file_version

* only capture first 3 numbers in semver

* build file version from tag

* updated workflow-dispatch
2025-04-24 14:05:07 +01:00
Mucahit Bilal GOKER b6a96802a8 Merge pull request #234 from specklesystems/bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version
Bilal/cnx 1507 load button in the model card should get new version
2025-04-23 14:40:23 +03:00
Mucahit Bilal GOKER f2a0ffa9ee Merge branch 'v3-dev' into bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version 2025-04-23 14:35:19 +03:00
Mucahit Bilal GOKER 24479811f7 Merge pull request #242 from specklesystems/bilal/cnx-1601-adding-models-by-url
Bilal/cnx 1601 adding models by url
2025-04-23 14:33:51 +03:00
Mucahit Bilal GOKER 2153db9704 fix: incorrect version id when model url is given 2025-04-22 23:33:54 +03:00
Mucahit Bilal GOKER dbcc820304 fix: load button in the model card loads latest version 2025-04-17 17:28:45 +03:00
Mucahit Bilal GOKER 830632fa1e ruff check: remove unused imports 2025-04-17 17:18:53 +03:00
Mucahit Bilal GOKER 2f57ca96ca added comments to main panel 2025-04-17 17:12:49 +03:00
Mucahit Bilal GOKER 1b9ee91880 select objects by model card collection name 2025-04-17 17:12:01 +03:00
Mucahit Bilal GOKER 8fb7519e7b load button -> set account id and collection name to model cards 2025-04-17 17:11:37 +03:00
Mucahit Bilal GOKER 9dce548a05 add collection name and account id to model cards 2025-04-17 17:10:24 +03:00
Mucahit Bilal GOKER 487253babe remove window manager invokes from dialogs 2025-04-17 17:08:17 +03:00
Mucahit Bilal GOKER f245584428 i forgot to add actual invoke method 2025-04-17 17:06:18 +03:00
Mucahit Bilal GOKER 1ac784d290 invoke window manager properties on initialization 2025-04-17 17:05:58 +03:00
Mucahit Bilal GOKER 314a962014 Merge branch 'v3-dev' into bilal/cnx-1507-load-button-in-the-model-card-should-get-new-version 2025-04-17 13:12:45 +03:00
Mucahit Bilal GOKER 6b07a0fff4 change button icon and remove text label 2025-04-17 12:50:14 +03:00
Mucahit Bilal GOKER d3208de754 error handling for the url 2025-04-17 12:38:44 +03:00
Mucahit Bilal GOKER 4ba19231b7 code cleanup 2025-04-17 11:57:27 +03:00
Mucahit Bilal GOKER a93ac797fc add project by url functionality 2025-04-16 19:31:06 +03:00
Mucahit Bilal GOKER 4569b1e623 ensure wm properties exists 2025-04-16 19:30:48 +03:00
Mucahit Bilal GOKER ae3222683e get model details by wrapper 2025-04-16 19:30:30 +03:00
Mucahit Bilal GOKER 780184c562 add button to project selection dialog 2025-04-16 18:06:09 +03:00
Dogukan Karatas 310b29292d Merge pull request #239 from specklesystems/bilal/ui-refactor
Bilal/UI refactor
2025-04-16 14:20:03 +02:00
Mucahit Bilal GOKER 8593200a48 Merge branch 'v3-dev' into bilal/ui-refactor 2025-04-16 14:57:48 +03:00
Mucahit Bilal GOKER e53ede3349 Merge branch 'bilal/ui-refactor' into v3-dev 2025-04-16 14:54:54 +03:00
Mucahit Bilal GOKER 892396f1e9 fix: resolve merge conflicts with dev 2025-04-16 14:45:23 +03:00
Dogukan Karatas c2ba9bc8f5 Merge pull request #240 from specklesystems/dogukan/cnx-1599-curve-and-polycurve-converters
feat: converter functions for curve, polycurve and point
2025-04-16 13:33:08 +02:00
Mucahit Bilal GOKER 3f8ec1d259 Merge pull request #241 from specklesystems/bilal/cnx-1590-add-desktop-service-auth-flow
Bilal/cnx 1590 add desktop service auth flow
2025-04-16 14:30:44 +03:00
Mucahit Bilal GOKER fd203bbfea added dynamic selection back 2025-04-16 14:16:33 +03:00
Dogukan Karatas 5ac0c64c9e code formatting 2025-04-16 13:09:37 +02:00
Mucahit Bilal GOKER db367d92cb force redraw of main panel on all selection dialogs 2025-04-16 12:29:12 +03:00
Mucahit Bilal GOKER 289ed58812 force redraw of main panel and close popup 2025-04-16 12:23:48 +03:00
Mucahit Bilal GOKER d1a65510ef Remove popup closing code from model card operators 2025-04-16 11:55:28 +03:00
Mucahit Bilal GOKER 4f58f77c92 Change model card settings dialog to popup window 2025-04-16 11:53:24 +03:00
Mucahit Bilal GOKER 10084b30da close popup after clicking any button on model card settings 2025-04-16 11:50:36 +03:00
Mucahit Bilal GOKER 88f1365fa6 close popup after deleting a model card 2025-04-16 11:34:04 +03:00
Dogukan Karatas f767f53c24 updates exception raise 2025-04-15 17:15:13 +02:00
Mucahit Bilal GOKER 9322e4bfe8 fix: resolve accounts property initialization in project selection dialog 2025-04-15 17:56:54 +03:00
Mucahit Bilal GOKER 31a9452ef0 dynamically update accounts list 2025-04-15 17:07:57 +03:00
Mucahit Bilal GOKER c69f70c7ac show only sign in button when no accounts available 2025-04-15 16:36:49 +03:00
Dogukan Karatas 7ff0778c7e adds curve, polycurve and point converter 2025-04-15 15:33:15 +02:00
Mucahit Bilal GOKER 0cd08b5cef fix: server url typo 2025-04-15 16:32:15 +03:00
Mucahit Bilal GOKER 46eed71a56 add account button next to account dropdown 2025-04-15 16:26:46 +03:00
Dogukan Karatas 623270117e Merge pull request #233 from specklesystems/bilal/cnx-1471-remove-model-card-option
Bilal/cnx 1471 remove model card option
2025-04-14 13:27:04 +02:00
Mucahit Bilal GOKER c5d081a765 clear window manager after load 2025-04-11 22:16:12 +03:00
Mucahit Bilal GOKER b2d9aba778 load button adds model cards 2025-04-11 22:16:02 +03:00
Mucahit Bilal GOKER 12b8bb20d5 get latest version by default after model selection 2025-04-11 22:02:47 +03:00
Mucahit Bilal GOKER 6e6214133a remove setting ui state from load button 2025-04-11 22:02:28 +03:00
Mucahit Bilal GOKER 2c35c4c1b2 version -> code reorg 2025-04-11 18:00:20 +03:00
Mucahit Bilal GOKER 80f9c54111 model -> code reorg 2025-04-11 18:00:05 +03:00
Mucahit Bilal GOKER 508cf6790f version button -> show latest is load option is latest 2025-04-11 17:59:49 +03:00
Mucahit Bilal GOKER 1c20949f58 reorg project selection dialog 2025-04-11 17:39:47 +03:00
Mucahit Bilal GOKER 88e17d66b8 model -> get account and project id from wm 2025-04-11 17:32:30 +03:00
Mucahit Bilal GOKER 625dd64f87 remove mouse mixin 2025-04-11 17:31:31 +03:00
Mucahit Bilal GOKER b22d94606c add load operation to load button 2025-04-10 17:46:18 +03:00
Mucahit Bilal GOKER 309a657420 resolve circular dependency 2025-04-10 17:45:59 +03:00
Mucahit Bilal GOKER 95d79391dd remove model card from load operation 2025-04-10 17:27:33 +03:00
Mucahit Bilal GOKER 49ec641f7d version selection dialog -> selected version id & load option 2025-04-10 17:24:14 +03:00
Mucahit Bilal GOKER 3410e1613b model selection dialog -> add selected_model_id and name 2025-04-10 16:54:22 +03:00
Mucahit Bilal GOKER b013855c97 project selection dialog -> set project id and name on execut 2025-04-10 16:49:20 +03:00
Mucahit Bilal GOKER 78f1a4e23e buttons for selecting project/model/version 2025-04-10 16:42:29 +03:00
Mucahit Bilal GOKER bf4afe0180 Merge branch 'v3-dev' into bilal/cnx-1471-remove-model-card-option 2025-04-09 17:56:05 +03:00
Mucahit Bilal GOKER 7714e8ba28 replace model card index approach with id 2025-04-09 17:39:27 +03:00
Mucahit Bilal GOKER 5ff0965ee6 remove confirmation dialog 2025-04-09 17:38:27 +03:00
Mucahit Bilal GOKER 6af53c56b3 make delete button red 2025-03-28 18:10:46 +03:00
Mucahit Bilal GOKER 100dc9213b delete self.report 2025-03-28 18:10:36 +03:00
Mucahit Bilal GOKER aad9246463 adds functional load button in the model card 2025-03-27 19:04:10 +03:00
Mucahit Bilal GOKER 0dbf691f16 adds delete model card button 2025-03-26 16:49:02 +03:00
55 changed files with 5540 additions and 1823 deletions
+15 -10
View File
@@ -26,14 +26,14 @@ jobs:
- 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
run: |
TAG=${{ github.ref_name }}
if [[ "${{ github.ref }}" != refs/tags/* ]]; then
TAG="3.0.99.${{ github.run_number }}"
TAG="v3.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/')
SEMVER="${TAG#v}"
FILE_VERSION=$(echo "$TAG" | sed -E 's/^v([0-9]+\.[0-9]+\.[0-9]+).*/\1/')
FILE_VERSION="$FILE_VERSION.${{ github.run_number }}"
echo "semver=$SEMVER" >> "$GITHUB_OUTPUT"
echo "fileVersion=$FILE_VERSION" >> "$GITHUB_OUTPUT"
@@ -69,16 +69,21 @@ jobs:
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'}}
IS_PUBLIC_RELEASE: ${{ github.ref_type == 'tag' }}
steps:
- name: 🔫 Trigger Build Installer(s)
uses: ALEEF02/workflow-dispatch@v3.0.0
uses: the-actions-org/workflow-dispatch@v4.0.0
with:
workflow: Build Blender Installer
workflow: Build Installers
repo: specklesystems/connector-installers
token: ${{ secrets.CONNECTORS_GH_TOKEN }}
inputs: '{ "run_id": "${{ github.run_id }}", "semver": "${{ needs.build.outputs.semver }}", "file_version": "${{ needs.build.outputs.fileVersion }}","public_release": ${{ env.IS_TAG_BUILD }}, "store_artifacts": ${{ env.IS_RELEASE_BRANCH }} }'
inputs: '{
"run_id": "${{ github.run_id }}",
"semver": "${{ needs.build.outputs.semver }}",
"file_version": "${{ needs.build.outputs.fileVersion }}",
"repo": "${{ github.repository }}",
"is_public_release": ${{ env.IS_PUBLIC_RELEASE }}
}'
ref: main
wait-for-completion: true
wait-for-completion-interval: 10s
+7 -22
View File
@@ -45,45 +45,30 @@ Give Speckle a try in no time by:
The Speckle UI can be found in the 3d viewport toolbar (N), under the Speckle tab.
Head to the [**📚 documentation**](https://speckle.guide/user/blender.html) for more information.
## Installation
We officially support Blender 3.3 and newer, on Windows and Mac.
Please follow our installation instructions on our [connector docs](https://speckle.guide/user/blender.html#installation)
We officially support Blender 4.2 and newer, on Windows.
## Usage
Once enabled in `Preferences -> Addons`,
The Speckle connector UI can be found in the 3d viewport toolbar (N), under the `Speckle` tab.
- Available user accounts are automatically detected and made available. To add user accounts use **Speckle Manager**.
- Select the user from the dropdown list in the `Users` panel. This will populate the `Projects` list with available projects for the selected user account.
- Available user accounts are automatically detected and made available.
- Select the account from the dropdown list in the `Accounts` panel. This will populate the `Projects` list with available projects for the selected user account.
- Select a model and version from the dropdown menus.
- Click on `Receive` to download and convert the objects from the selected model version. The objects will be linked into a Blender Collection, named `<PROJECT_NAME> [ <MODEL_NAME> @ <VERSION_ID> ]`.
- Click on `Load` to download and convert the objects from the selected model version. The objects will be linked into a Blender Collection.
- Click on `Open Model in Web` to view the model in your web browser.
## Supported Elements
The Blender Connector is still a work in progress and, as such, data sent from the Blender connector is a highly lossy exchange. Our connectors are ever evolving to facilitate more and more Speckle usecases. We welcome feedback, requests, edge cases, and contributions!
The full matrix of supported Blender and Speckle types [can be found here](https://speckle.guide/user/support-tables.html#blender)
## Additional Features
- Speckle properties will be imported as custom properties on Blender objects. Nested dictionaries are expanded to individual properties by flattening their key hierarchy. I.e. `propA:{'propB': {'propC':10, 'propD':'foobar'}}` is flattened to `propA.propB.propC = 10` and `propA.propB.propD = "foobar"`.
- If a `renderMaterial` property is found, **SpeckleBlender** will create a material named using the sub-property `renderMaterial.name`. If a material with that name already exists in Blender, **SpeckleBlender** will just assign that existing material to the object. This allows geometry to be updated without having to re-assign and re-create materials.
- Receiving vertex colors is supported. The `colors` list from Speckle meshes is translated to a vertex color layer.
- Receive/Send scripts. Allow injecting a custom python function to the receive/send process to automate any blender operations
## Dependency Installation and Compatibility with Other Blender Addons
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows 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.
Upon first launch of the addon, the Speckle connector installs its SpecklePy dependencies in `%appdata%/Speckle/connector_installations` on Windows.
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.
+151 -40
View File
@@ -12,53 +12,133 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ruff: noqa
import bpy
from bpy.types import WindowManager
from .connector.ui import icons
import json
# Ensure dependencies
from .installer import ensure_dependencies
ensure_dependencies(f"Blender {bpy.app.version[0]}.{bpy.app.version[1]}")
bl_info = {
"name": "Speckle Blender ",
"author": "Speckle Systems",
"name": "Speckle Connector",
"author": "Speckle",
"version": (3, 999, 999),
"blender": (4, 2, 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",
"wiki_url": "https://github.com/specklesystems/speckle-blender",
"description": "Publish models to and load models from other AEC apps.",
"wiki_url": "https://speckle.systems/connectors/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.utils.account_manager import speckle_workspace
from .connector.ui.project_selection_dialog import (
SPECKLE_OT_project_selection_dialog,
SPECKLE_UL_projects_list,
)
from .connector.ui.model_selection_dialog import (
SPECKLE_OT_model_selection_dialog,
SPECKLE_UL_models_list,
)
from .connector.ui.version_selection_dialog import (
SPECKLE_OT_version_selection_dialog,
SPECKLE_UL_versions_list,
)
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog
from .connector.ui.model_card import speckle_model_card
from .connector.utils.property_groups import (
speckle_project,
speckle_model,
speckle_version,
speckle_object,
speckle_collection,
speckle_model_card,
)
# Operators
from .connector.blender_operators.publish_button import SPECKLE_OT_publish
from .connector.blender_operators.load_button import SPECKLE_OT_load
from .connector.blender_operators.model_card_settings import SPECKLE_OT_model_card_settings, SPECKLE_OT_view_in_browser, SPECKLE_OT_view_model_versions
from .connector.blender_operators.model_card_settings import (
SPECKLE_OT_model_card_settings,
SPECKLE_OT_view_in_browser,
SPECKLE_OT_view_model_versions,
SPECKLE_OT_delete_model_card,
)
from .connector.blender_operators.select_objects import SPECKLE_OT_select_objects
from .connector.blender_operators.add_account_button import SPECKLE_OT_add_account
from .connector.blender_operators.model_card_load_button import (
SPECKLE_OT_load_model_card,
)
from .connector.blender_operators.model_card_publish_button import (
SPECKLE_OT_publish_model_card,
)
from .connector.blender_operators.add_project_by_url import (
SPECKLE_OT_add_project_by_url,
)
from .connector.blender_operators.create_project import SPECKLE_OT_create_project
from .connector.blender_operators.create_model import SPECKLE_OT_create_model
from .connector.utils.account_manager import (
speckle_account,
get_default_account_id,
_client_cache,
)
# States
from .connector.states.speckle_state import register as register_speckle_state, unregister as unregister_speckle_state
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)
from .connector.ui.workspace_selection_dialog import (
SPECKLE_OT_workspace_selection_dialog,
SPECKLE_UL_workspaces_list,
)
# Utils
from .connector.ui.account_selection_dialog import (
SPECKLE_OT_account_selection_dialog,
SPECKLE_UL_accounts_list,
)
def invoke_window_manager_properties():
# Accounts
WindowManager.speckle_accounts = bpy.props.CollectionProperty(type=speckle_account)
WindowManager.selected_account_id = bpy.props.StringProperty()
# Workspaces
WindowManager.speckle_workspaces = bpy.props.CollectionProperty(
type=speckle_workspace
)
WindowManager.selected_workspace = bpy.props.PointerProperty(type=speckle_workspace)
WindowManager.can_create_project_in_workspace = bpy.props.BoolProperty()
# Projects
WindowManager.speckle_projects = bpy.props.CollectionProperty(type=speckle_project)
WindowManager.selected_project_id = bpy.props.StringProperty()
WindowManager.selected_project_name = bpy.props.StringProperty()
# Models
WindowManager.speckle_models = bpy.props.CollectionProperty(type=speckle_model)
WindowManager.selected_model_id = bpy.props.StringProperty()
WindowManager.selected_model_name = bpy.props.StringProperty()
# Versions
WindowManager.speckle_versions = bpy.props.CollectionProperty(type=speckle_version)
WindowManager.selected_version_id = bpy.props.StringProperty()
WindowManager.selected_version_load_option = bpy.props.StringProperty()
# Send / Publish buttons
WindowManager.ui_mode = bpy.props.EnumProperty( # type: ignore
name="UI Mode",
description="Publish or Load a model",
items=[
("PUBLISH", "Publish", "Publish a model to Speckle", "EXPORT", 0),
("LOAD", "Load", "Load a model from Speckle", "IMPORT", 1),
],
default="PUBLISH",
)
# Objects
WindowManager.speckle_objects = bpy.props.CollectionProperty(type=speckle_object)
# Classes to load
@@ -66,20 +146,38 @@ 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_project_selection_dialog,
speckle_project,
SPECKLE_UL_projects_list,
speckle_workspace,
SPECKLE_OT_model_selection_dialog,
speckle_model,
SPECKLE_UL_models_list,
SPECKLE_OT_version_selection_dialog,
speckle_version,
SPECKLE_UL_versions_list,
SPECKLE_OT_selection_filter_dialog,
speckle_model_card, SPECKLE_OT_model_card_settings, SPECKLE_OT_view_in_browser, SPECKLE_OT_view_model_versions,
SPECKLE_OT_select_objects)
speckle_object,
speckle_collection,
speckle_model_card,
SPECKLE_OT_model_card_settings,
SPECKLE_OT_view_in_browser,
SPECKLE_OT_view_model_versions,
SPECKLE_OT_delete_model_card,
SPECKLE_OT_select_objects,
SPECKLE_OT_add_account,
SPECKLE_OT_load_model_card,
SPECKLE_OT_publish_model_card,
SPECKLE_OT_add_project_by_url,
SPECKLE_OT_create_project,
SPECKLE_OT_create_model,
speckle_account,
SPECKLE_UL_workspaces_list,
SPECKLE_OT_workspace_selection_dialog,
SPECKLE_OT_account_selection_dialog,
SPECKLE_UL_accounts_list,
)
@bpy.app.handlers.persistent
def load_handler(dummy):
load_model_cards(bpy.context.scene)
@bpy.app.handlers.persistent
def save_handler(dummy):
save_model_cards(bpy.context.scene)
# Register and Unregister
def register():
@@ -87,19 +185,32 @@ def register():
for cls in classes:
bpy.utils.register_class(cls)
register_speckle_state() # Register SpeckleState
register_speckle_state() # Register SpeckleState
invoke_window_manager_properties()
# Pre-warm client cache for default account
try:
default_account_id = get_default_account_id()
if default_account_id:
print(
f"[Speckle] Pre-warming client for default account: {default_account_id}"
)
_client_cache.get_client(default_account_id)
print(
f"[Speckle] Client pre-warming complete for account: {default_account_id}"
)
except Exception as e:
print(f"[Speckle] Failed to pre-warm client: {e}")
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
unregister_speckle_state() # Unregister SpeckleState
_client_cache.clear()
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.app.handlers.load_post.remove(load_handler)
bpy.app.handlers.save_post.remove(save_handler)
# Run the register function when the script is executed
if __name__ == "__main__":
+7 -15
View File
@@ -4,14 +4,14 @@ schema_version = "1.0.0"
# 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"
name = "Speckle Connector"
tagline = "Load models from other AEC apps into Blender with Speckle."
maintainer = "Speckle"
# Supported types: "add-on", "theme"
type = "add-on"
# Optional link to documentation, support, source files, etc
website = "https://speckle.guide/user/blender.html"
website = "https://speckle.systems/connectors/blender"
# Optional list defined by Blender and server, see:
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
@@ -24,13 +24,9 @@ blender_version_min = "4.2.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",
]
license = ["SPDX:Apache-2.0"]
# Optional: required by some licenses.
copyright = [
"2022-2025 AEC SYSTEMS LTD",
]
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"]
@@ -67,8 +63,4 @@ 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",
]
paths_exclude_pattern = ["__pycache__/", "/.vscode", "*.code-workspace"]
@@ -1,3 +1,9 @@
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
from ..blender_operators.load_button import SPECKLE_OT_load # noqa: F401
from .model_card_load_button import SPECKLE_OT_load_model_card # noqa: F401
from ..blender_operators.publish_button import SPECKLE_OT_publish # noqa: F401
from ..blender_operators.model_card_settings import (
SPECKLE_OT_model_card_settings, # noqa: F401
SPECKLE_OT_view_in_browser, # noqa: F401
SPECKLE_OT_view_model_versions, # noqa: F401
SPECKLE_OT_delete_model_card, # noqa: F401
)
@@ -0,0 +1,38 @@
import bpy
import webbrowser
from bpy.types import Event, Context
class SPECKLE_OT_add_account(bpy.types.Operator):
"""Operator for adding a new Speckle account."""
bl_idname = "speckle.add_account"
bl_label = "Add New Account"
bl_description = "Add a new account"
server_url: bpy.props.StringProperty( # type: ignore
name="Server URL",
description="Speckle server URL to connect to",
default="https://app.speckle.systems",
)
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context):
layout = self.layout
# Server URL textbox
layout.prop(self, "server_url", text="Server URL")
def execute(self, context: Context) -> set[str]:
# Logic to handle sign in
api_url = "http://localhost:29364"
url = f"{api_url}/auth/add-account?serverUrl={self.server_url}"
webbrowser.open(url)
self.report({"INFO"}, f"Adding account from {self.server_url}: {url}")
# Force redraw
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
@@ -0,0 +1,75 @@
import bpy
from bpy.types import Context, Event, UILayout
from ..utils.account_manager import (
get_model_details_by_wrapper,
get_project_from_url,
can_load,
)
class SPECKLE_OT_add_project_by_url(bpy.types.Operator):
"""
operator for adding a Speckle project by URL
"""
bl_idname = "speckle.add_project_by_url"
bl_label = "Add Project by URL"
bl_description = "Add a project from a URL"
url: bpy.props.StringProperty( # type: ignore
name="Project URL", description="Enter the Speckle project URL", default=""
)
def execute(self, context: Context) -> set[str]:
self.report({"INFO"}, f"Adding project from URL: {self.url}")
wm = context.window_manager
# Get project from URL
wrapper, client, project, error_message = get_project_from_url(self.url)
if error_message:
self.report({"ERROR"}, error_message)
return {"CANCELLED"}
# Get model details from the wrapper
(
account_id,
project_id,
project_name,
model_id,
model_name,
version_id,
load_option,
) = get_model_details_by_wrapper(wrapper)
# Check permissions
can_load_permission, permission_error = can_load(client, project)
if not can_load_permission:
self.report({"ERROR"}, permission_error)
return {"CANCELLED"}
# Update the window manager with the selected project/model/version
wm.selected_account_id = account_id
if project_id:
wm.selected_project_id = project_id
wm.selected_project_name = project_name
if model_id:
wm.selected_model_id = model_id
wm.selected_model_name = model_name
if version_id:
wm.selected_version_id = version_id
wm.selected_version_id = version_id
wm.selected_version_load_option = load_option
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.prop(self, "url", text="")
@@ -0,0 +1,68 @@
import bpy
from bpy.types import Context, Event, UILayout
from specklepy.core.api.inputs import CreateModelInput
from specklepy.core.api.models import Model
from ..utils.account_manager import _client_cache
class SPECKLE_OT_create_model(bpy.types.Operator):
bl_idname = "speckle.create_model"
bl_label = "Create Model"
bl_description = "Create a new Speckle model"
model_name: bpy.props.StringProperty(name="Model Name") # type: ignore
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
if not self.model_name.strip():
self.report({"ERROR"}, "Model name cannot be empty")
return {"CANCELLED"}
try:
model = _create_model(
wm.selected_account_id, wm.selected_project_id, self.model_name
)
wm.selected_model_id = model.id
wm.selected_model_name = model.name
self.report({"INFO"}, f"Created model: {model.name} -> ID: {model.id}")
# Force redraw
context.window.screen = context.window.screen
context.area.tag_redraw()
except Exception as e:
self.report({"ERROR"}, f"Failed to create model: {str(e)}")
return {"CANCELLED"}
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.prop(self, "model_name")
def register() -> None:
bpy.utils.register_class(SPECKLE_OT_create_model)
def unregister() -> None:
bpy.utils.unregister_class(SPECKLE_OT_create_model)
def _create_model(account_id: str, project_id: str, model_name: str) -> Model:
try:
# Get cached client
client = _client_cache.get_client(account_id)
model = client.model.create(
input=CreateModelInput(
name=model_name, description="", project_id=project_id
)
)
return model
except Exception as e:
# Clear cache on error to prevent stale clients
_client_cache.clear()
raise e
@@ -0,0 +1,87 @@
from typing import Optional
import bpy
from bpy.types import Context, Event, UILayout
from specklepy.core.api.enums import ProjectVisibility
from specklepy.core.api.inputs import ProjectCreateInput
from specklepy.core.api.inputs.project_inputs import WorkspaceProjectCreateInput
from specklepy.core.api.models import Project
from ..utils.account_manager import _client_cache
class SPECKLE_OT_create_project(bpy.types.Operator):
"""
operator for adding a Speckle project by URL
"""
bl_idname = "speckle.create_project"
bl_label = "Create Project"
bl_description = "Create a new Speckle project"
project_name: bpy.props.StringProperty(name="Project Name") # type: ignore
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
project = _create_project(
wm.selected_account_id,
self.project_name,
None
if wm.selected_workspace.id == "personal"
else wm.selected_workspace.id,
)
wm.selected_project_id = project.id
wm.selected_project_name = project.name
self.report({"INFO"}, f"Created project: {project.name} -> ID: {project.id}")
# Force redraw
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.prop(self, "project_name")
def register() -> None:
bpy.utils.register_class(SPECKLE_OT_create_project)
def unregister() -> None:
bpy.utils.unregister_class(SPECKLE_OT_create_project)
def _create_project(
account_id: str, project_name: str, workspace_id: Optional[str]
) -> Project:
try:
# Get cached client
client = _client_cache.get_client(account_id)
if workspace_id:
project = client.project.create_in_workspace(
input=WorkspaceProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility.PUBLIC,
workspaceId=workspace_id,
)
)
else:
project = client.project.create(
input=ProjectCreateInput(
name=project_name,
description="",
visibility=ProjectVisibility.PUBLIC,
)
)
return project
except Exception as e:
print(f"Failed to create project: {str(e)}")
# Clear cache on error to prevent stale clients
_client_cache.clear()
raise
@@ -1,27 +1,79 @@
import bpy
from typing import Set
from bpy.types import Context
from bpy.types import Context, Event
from ..operations.load_operation import load_operation
from ..utils.account_manager import get_server_url_by_account_id
from ..utils.model_card_utils import (
update_model_card_objects,
delete_model_card_objects,
model_card_exists,
)
class SPECKLE_OT_load(bpy.types.Operator):
bl_idname = "speckle.load"
bl_label = "Load from Speckle"
bl_description = "Load objects from Speckle"
bl_label = "Load model"
bl_description = "Load selection 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)
instance_loading_mode: bpy.props.EnumProperty( # type: ignore
name="Instance Loading",
description="Choose how to load instances",
items=[
(
"INSTANCE_PROXIES",
"Collection Instances",
"Load objects as collection instances",
),
(
"LINKED_DUPLICATES",
"Linked Duplicates",
"Get objects as linked duplicates",
),
],
default="INSTANCE_PROXIES",
)
def draw(self, context: Context) -> None:
layout = self.layout
row = layout.row()
row.label(text="Instance Loading:")
row.prop(self, "instance_loading_mode", text="")
def invoke(self, context: Context, event: Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
# 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")
wm = context.window_manager
if model_card_exists(
wm.selected_project_id, wm.selected_model_id, False, context
):
model_card = context.scene.speckle_state.get_model_card_by_id(
f"{wm.ui_mode}-{wm.selected_project_id}-{wm.selected_model_id}"
)
delete_model_card_objects(model_card, context)
else:
model_card = context.scene.speckle_state.model_cards.add()
model_card.account_id = wm.selected_account_id
model_card.server_url = get_server_url_by_account_id(wm.selected_account_id)
model_card.project_id = wm.selected_project_id
model_card.project_name = wm.selected_project_name
model_card.model_id = wm.selected_model_id
model_card.model_name = wm.selected_model_name
model_card.is_publish = False
model_card.load_option = wm.selected_version_load_option
model_card.version_id = wm.selected_version_id
model_card.instance_loading_mode = self.instance_loading_mode
converted_objects = load_operation(context, self.instance_loading_mode)
update_model_card_objects(model_card, converted_objects)
# Clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_project_name = ""
wm.selected_model_id = ""
wm.selected_model_name = ""
wm.selected_version_load_option = ""
wm.selected_version_id = ""
return {"FINISHED"}
@@ -0,0 +1,81 @@
import bpy
from typing import Set
from bpy.types import Context
from ..utils.version_manager import get_latest_version
from ..operations.load_operation import load_operation
from ..utils.model_card_utils import (
delete_model_card_objects,
update_model_card_objects,
collect_objects_with_properties,
)
class SPECKLE_OT_load_model_card(bpy.types.Operator):
bl_idname = "speckle.model_card_load"
bl_label = "Load Latest from Speckle"
bl_description = "Depending on the load option, loads the latest or a specific version from Speckle"
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type: ignore
def execute(self, context: Context) -> Set[str]:
wm = context.window_manager
# Get the model card
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
old_properties = collect_objects_with_properties(model_card)
delete_model_card_objects(model_card, context)
# set wm
wm.selected_account_id = model_card.account_id
wm.selected_project_id = model_card.project_id
wm.selected_model_name = model_card.model_name
# if load option is set to "LATEST"
if model_card.load_option == "LATEST":
# get latest version from speckle
latest_version_id, message, timestamp = get_latest_version(
model_card.account_id, model_card.project_id, model_card.model_id
)
# set version id in wm
wm.selected_version_id = latest_version_id
# load latest version
converted_objects = load_operation(
context, model_card.instance_loading_mode
)
# update model card details
update_model_card_objects(model_card, converted_objects, old_properties)
model_card.version_id = latest_version_id
else:
# set version id in wm
wm.selected_version_id = model_card.version_id
# load version id
converted_objects = load_operation(
context, model_card.instance_loading_mode
)
if not converted_objects:
self.report({"ERROR"}, "Load operation failed")
return {"CANCELLED"}
# update model card details
update_model_card_objects(model_card, converted_objects, old_properties)
# Clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_version_id = ""
wm.selected_model_name = ""
self.report(
{"INFO"},
f"{len(converted_objects)} objects loaded from Speckle. Model: {model_card.model_name}, Version: {model_card.version_id}",
)
return {"FINISHED"}
@@ -0,0 +1,72 @@
import bpy
from typing import Set
from bpy.types import Context, Event
from ..operations.publish_operation import publish_operation
class SPECKLE_OT_publish_model_card(bpy.types.Operator):
bl_idname = "speckle.model_card_publish"
bl_label = "Publish model"
bl_description = "Publish tracked objects to Speckle"
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type: ignore
version_message: bpy.props.StringProperty(name="Version Message", default="") # type: ignore
def draw(self, context: Context) -> None:
layout = self.layout
layout.prop(self, "version_message")
def invoke(self, context: Context, event: Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
wm = context.window_manager
# Get the model card
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
# set wm
wm.selected_account_id = model_card.account_id
wm.selected_project_id = model_card.project_id
wm.selected_model_id = model_card.model_id
# get model card objects
objects_to_convert = []
for speckle_obj in model_card.objects:
blender_obj = bpy.data.objects.get(speckle_obj.name)
if blender_obj:
objects_to_convert.append(blender_obj)
else:
self.report(
{"WARNING"}, f"Object '{speckle_obj.name}' not found, skipping"
)
if not objects_to_convert:
self.report({"ERROR"}, "No objects to publish")
return {"CANCELLED"}
# publish to speckle
success, message, version_id = publish_operation(
context,
objects_to_convert,
self.version_message,
model_card.apply_modifiers,
)
if not success:
self.report({"ERROR"}, message)
return {"CANCELLED"}
model_card.version_id = version_id
model_card.is_publish = True
# Clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_model_id = ""
self.report({"INFO"}, message)
return {"FINISHED"}
@@ -3,70 +3,125 @@ 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.
"""
manages settings and actions for a Speckle model card
"""
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
bl_description = "More options for the model card"
model_card_id: bpy.props.StringProperty(name="Model Card ID", default="") # type:ignore
def execute(self, context: Context) -> Set[str]:
self.report({'INFO'}, f"Settings for {context.scene.speckle_state.model_cards[self.model_card_index]}")
return {'FINISHED'}
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
layout.operator(
"speckle.view_in_browser", text="View in Browser"
).model_card_id = self.model_card_id
layout.operator(
"speckle.view_model_versions", text="View Model Versions"
).model_card_id = self.model_card_id
layout.separator()
row = layout.row()
# add a button for deleting the model card
row.alert = True
delete_op = row.operator(
"speckle.delete_model_card", text="Delete Model Card", icon="TRASH"
)
delete_op.model_card_id = self.model_card_id
def invoke(self, context: Context, event: Event) -> Set[str]:
wm = context.window_manager
return wm.invoke_props_dialog(self)
return wm.invoke_popup(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.
"""
opens the current model in the Speckle web viewer
"""
bl_idname = "speckle.view_in_browser"
bl_label = "View in Browser"
bl_description = "View the model in the browser"
model_card_index: bpy.props.IntProperty() #type: ignore
model_card_id: bpy.props.StringProperty() # type: ignore
def execute(self, context: Context) -> Set[str]:
model_card = context.scene.speckle_state.model_cards[self.model_card_index]
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
url = f"{model_card.server_url}/projects/{model_card.project_id}/models/{model_card.model_id}"
webbrowser.open(url)
self.report({'INFO'}, f"Viewing in the browser: {url}")
return {'FINISHED'}
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.
"""
opens the model's version history in the Speckle web app
"""
bl_idname = "speckle.view_model_versions"
bl_label = "View Model Versions"
bl_description = "View the model versions in the browser"
model_card_index: bpy.props.IntProperty() #type: ignore
model_card_id: bpy.props.StringProperty() # type: ignore
def execute(self, context: Context) -> Set[str]:
model_card = context.scene.speckle_state.model_cards[self.model_card_index]
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
url = f"{model_card.server_url}/projects/{model_card.project_id}/models/{model_card.model_id}/versions"
webbrowser.open(url)
self.report({'INFO'}, "Viewing model's versions in the browser")
return {'FINISHED'}
self.report({"INFO"}, "Viewing model's versions in the browser")
return {"FINISHED"}
class SPECKLE_OT_delete_model_card(bpy.types.Operator):
"""
deletes a Speckle model card from the Blender UI
"""
bl_idname = "speckle.delete_model_card"
bl_label = "Delete Model Card"
bl_description = "Delete this model card"
model_card_id: bpy.props.StringProperty() # type: ignore
def execute(self, context: Context) -> Set[str]:
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
model_name = model_card.model_name
# find the index of the model card and remove it
for i, card in enumerate(context.scene.speckle_state.model_cards):
if card.get_model_card_id() == self.model_card_id:
context.scene.speckle_state.model_cards.remove(i)
break
self.report({"INFO"}, f"Model card '{model_name}' has been deleted")
context.window.screen = context.window.screen
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> Set[str]:
return self.execute(context)
@@ -3,23 +3,115 @@ from bpy.types import Context
from bpy.types import Event
from typing import Set
# Publish Operator
from ..operations.publish_operation import publish_operation
from ..utils.account_manager import get_server_url_by_account_id
from ..utils.model_card_utils import model_card_exists, update_model_card_objects
class SPECKLE_OT_publish(bpy.types.Operator):
bl_idname = "speckle.publish"
bl_label = "Publish to Speckle"
bl_description = "Publish selected objects to Speckle"
version_message: bpy.props.StringProperty(name="Version Message") # type: ignore
apply_modifiers: bpy.props.BoolProperty( # type: ignore
name="Apply Modifiers",
description="Apply all modifiers to objects before conversion",
default=True,
)
def draw(self, context: Context) -> None:
layout = self.layout
layout.prop(self, "version_message")
layout.prop(self, "apply_modifiers")
def invoke(self, context: Context, event: Event) -> Set[str]:
# Captures the mouse position for UI placement
context.scene.speckle_state.mouse_position = (event.mouse_x, event.mouse_y)
return self.execute(context)
return context.window_manager.invoke_props_dialog(self)
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'}
wm = context.window_manager
# check if we have stored objects from selection dialog
if not wm.speckle_objects:
self.report(
{"ERROR"},
"No objects selected to publish. Please use 'Select Objects' first.",
)
return {"CANCELLED"}
account_id = getattr(wm, "selected_account_id", "")
project_id = getattr(wm, "selected_project_id", "")
model_id = getattr(wm, "selected_model_id", "")
if not account_id:
self.report({"ERROR"}, "No account selected")
return {"CANCELLED"}
if not project_id:
self.report({"ERROR"}, "No project selected")
return {"CANCELLED"}
if not model_id:
self.report({"ERROR"}, "No model selected")
return {"CANCELLED"}
objects_to_convert = []
for speckle_obj in wm.speckle_objects:
blender_obj = bpy.data.objects.get(speckle_obj.name)
if blender_obj:
objects_to_convert.append(blender_obj)
else:
self.report(
{"WARNING"}, f"Object '{speckle_obj.name}' not found, skipping"
)
if not objects_to_convert:
self.report({"ERROR"}, "None of the selected objects could be found")
return {"CANCELLED"}
success, message, version_id = publish_operation(
context, objects_to_convert, self.version_message, self.apply_modifiers
)
if not success:
self.report({"ERROR"}, message)
return {"CANCELLED"}
# create model card if operation was successful
if hasattr(context.scene, "speckle_state") and hasattr(
context.scene.speckle_state, "model_cards"
):
if model_card_exists(
wm.selected_project_id, wm.selected_model_id, True, context
):
model_card = context.scene.speckle_state.get_model_card_by_id(
f"{wm.ui_mode}-{wm.selected_project_id}-{wm.selected_model_id}"
)
else:
model_card = context.scene.speckle_state.model_cards.add()
model_card.account_id = account_id
model_card.server_url = get_server_url_by_account_id(account_id)
model_card.project_id = project_id
model_card.project_name = getattr(wm, "selected_project_name", "")
model_card.model_id = model_id
model_card.model_name = getattr(wm, "selected_model_name", "")
model_card.is_publish = True
model_card.load_option = "SPECIFIC" # published versions are specific
model_card.version_id = version_id
model_card.apply_modifiers = self.apply_modifiers
update_model_card_objects(model_card, objects_to_convert)
# clear selected model details from Window Manager
wm.selected_account_id = ""
wm.selected_project_id = ""
wm.selected_project_name = ""
wm.selected_model_id = ""
wm.selected_model_name = ""
wm.selected_version_load_option = ""
wm.selected_version_id = ""
wm.speckle_objects.clear()
self.report({"INFO"}, message)
context.area.tag_redraw()
return {"FINISHED"}
@@ -1,50 +1,34 @@
import bpy
from bpy.types import Operator
from bpy.props import IntProperty
from bpy.props import StringProperty
from ..utils.model_card_utils import select_model_card_objects, zoom_to_selected_objects
class SPECKLE_OT_select_objects(Operator):
"""Select all objects imported from this Speckle model"""
"""
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
bl_options = {"REGISTER", "UNDO"}
bl_description = (
"Selects and zooms extents to objects loaded from this Speckle model"
)
model_card_id: StringProperty(
name="Model Card ID", description="ID of the model card", default=""
) # type: ignore
def execute(self, context):
# 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'}
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, "Model card not found")
return {"CANCELLED"}
select_model_card_objects(model_card, context)
zoom_to_selected_objects(context)
self.report({"INFO"}, f"Selected {len(context.selected_objects)} objects")
return {"FINISHED"}
+1 -1
View File
@@ -1,2 +1,2 @@
from ..blender_operators.load_button import SPECKLE_OT_load # noqa: F401
from ..operations.load_operation import load_operation # noqa: F401
from ..operations.publish_operation import publish_operation # noqa: F401
@@ -1,90 +1,118 @@
from typing import Dict, Union
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.core.api import host_applications, operations
from specklepy.logging import metrics
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
from specklepy.objects.models.collections.collection import Collection as SCollection
from specklepy.transports.server import ServerTransport
from ... import bl_info
from ...converter.to_native import (
convert_to_native,
find_instance_definitions,
instance_definition_proxy_to_native,
render_material_proxy_to_native,
)
from ...converter.utils import find_object_by_id
from ..utils.account_manager import _client_cache
from ..utils.get_ascendants import get_ascendants
from ...converter.to_native import convert_to_native, render_material_proxy_to_native
def load_operation(context: Context, model_card) -> None:
def load_operation(
context: Context, instance_loading_mode: str = "INSTANCE_PROXIES"
) -> Dict[str, Union[bpy.types.Collection, bpy.types.Object]]:
"""
load objects from Speckle and maintain hierarchy.
"""
# 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,
)
wm = context.window_manager
if account is None:
print("No Speckle account found")
return
# get cached client
client = _client_cache.get_client(context.window_manager.selected_account_id)
# initialize the Speckle client
client = SpeckleClient(host=account.serverInfo.url)
# authenticate with account
client.authenticate_with_account(account)
print(f"Using client for account: {context.window_manager.selected_account_id}")
# create a transport
transport = ServerTransport(stream_id=model_card.project_id, client=client)
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
# get the version
version = client.version.get(model_card.version_id, model_card.project_id)
version = client.version.get(wm.selected_version_id, wm.selected_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")
metrics.set_host_app("blender")
metrics.track(
metrics.RECEIVE,
client.account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"sourceHostApp": host_applications.get_host_app_from_string(
version.source_application
).slug,
"isMultiplayer": version.author_user.id != client.account.userInfo.id,
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
},
)
# Create material mapping first
material_mapping = render_material_proxy_to_native(version_data)
definition_collections, definition_objects = instance_definition_proxy_to_native(
version_data, material_mapping, instance_loading_mode=instance_loading_mode
)
definitions_root_collection = None
if definition_collections:
definitions_root_collection = bpy.data.collections.new("InstanceDefinitions")
for collection in definition_collections.values():
definitions_root_collection.children.link(collection)
definition_object_ids = set()
for definition in find_instance_definitions(version_data).values():
definition_object_ids.update(definition.objects)
for obj_id in definition.objects:
found_obj = find_object_by_id(version_data, obj_id)
if found_obj:
if hasattr(found_obj, "id"):
definition_object_ids.add(found_obj.id)
if hasattr(found_obj, "applicationId"):
definition_object_ids.add(found_obj.applicationId)
# 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_name = f"{wm.selected_model_name} - {wm.selected_version_id}"
root_collection = bpy.data.collections.new(root_collection_name)
context.scene.collection.children.link(root_collection)
# 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
converted_objects = definition_objects.copy()
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"):
# Skip objects that are part of instance definitions
if speckle_obj.id in definition_object_ids or (
hasattr(speckle_obj, "applicationId")
and speckle_obj.applicationId in definition_object_ids
):
continue
# store all objects for later reference
all_objects[speckle_obj.id] = speckle_obj
# get all ascendants in order (current to root)
@@ -92,48 +120,36 @@ def load_operation(context: Context, model_card) -> None:
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]}"
speckle_obj, "name", f"Collection_{speckle_obj.id}"
)
# 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,
"applicationId": getattr(speckle_obj, "applicationId", ""),
"blender_collection": None,
"full_path": [collection_name], # start the path with this collection
"full_path": [collection_name],
}
# 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
pass
# 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:
@@ -142,7 +158,6 @@ def load_operation(context: Context, model_card) -> None:
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: (
@@ -151,14 +166,12 @@ def load_operation(context: Context, model_card) -> None:
),
)
# 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
@@ -167,26 +180,24 @@ def load_operation(context: Context, model_card) -> None:
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:
print(f"Collection already exists: {coll_name}")
blender_collection = created_collections[collection_key]
else:
blender_collection = bpy.data.collections.new(coll_name)
if coll_info.get("applicationId"):
blender_collection["applicationId"] = coll_info["applicationId"]
parent_collection.children.link(blender_collection)
created_collections[collection_key] = blender_collection
# store the created collection
coll_info["blender_collection"] = blender_collection
converted_objects[coll_id] = blender_collection
@@ -194,30 +205,27 @@ def load_operation(context: Context, model_card) -> None:
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:
if not hasattr(speckle_obj, "id"):
print("Skipping object without ID")
continue
# Skip objects that are part of instance definitions
if speckle_obj.id in definition_object_ids or (
hasattr(speckle_obj, "applicationId")
and speckle_obj.applicationId in definition_object_ids
):
continue
if speckle_obj.id in converted_objects:
continue
try:
# 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
@@ -227,19 +235,33 @@ def load_operation(context: Context, model_card) -> None:
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
blender_obj = convert_to_native(
speckle_obj,
material_mapping,
definition_collections=definition_collections,
root_collection=target_collection,
instance_loading_mode=instance_loading_mode,
)
if not already_linked:
target_collection.objects.link(blender_obj)
if blender_obj is None:
continue
except RuntimeError as e:
print(f"Error linking object to collection: {e}")
converted_objects[speckle_obj.id] = blender_obj
if hasattr(speckle_obj, "applicationId"):
converted_objects[speckle_obj.applicationId] = blender_obj
if not isinstance(blender_obj, bpy.types.Collection):
try:
already_linked = False
for coll in bpy.data.collections:
if blender_obj.name in coll.objects:
already_linked = True
if not already_linked:
target_collection.objects.link(blender_obj)
except RuntimeError as e:
print(f"Error linking object to collection: {e}")
except Exception as e:
print(f"Error converting {speckle_obj.speckle_type}: {str(e)}")
@@ -247,17 +269,16 @@ def load_operation(context: Context, model_card) -> None:
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.")
print(f"\nLoad process completed. Imported {len(converted_objects)} objects.")
return converted_objects
@@ -0,0 +1,324 @@
from typing import Dict, List, Optional, Tuple
import bpy
from bpy.types import Collection as BlenderCollection
from bpy.types import Context
from specklepy.core.api import operations
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
from specklepy.logging import metrics
from specklepy.objects import Base
from specklepy.objects.models.collections.collection import Collection
from specklepy.objects.models.units import Units
from specklepy.transports.server import ServerTransport
from ... import bl_info
from ...converter.to_speckle import convert_to_speckle
from ...converter.to_speckle.material_to_speckle import (
add_render_material_proxies_to_base,
)
from ..utils.account_manager import _client_cache
def publish_operation(
context: Context,
objects_to_convert: List,
version_message: str = "",
apply_modifiers: bool = True,
) -> Tuple[bool, str, Optional[str]]:
"""
publish objects to speckle
"""
wm = context.window_manager
try:
# get cached client
client = _client_cache.get_client(wm.selected_account_id)
transport = ServerTransport(stream_id=wm.selected_project_id, client=client)
# build collection hierarchy and convert objects
root_collection = build_collection_hierarchy(
context, objects_to_convert, apply_modifiers
)
if not root_collection:
return False, "No objects could be converted to Speckle format", None
# add material proxies
add_render_material_proxies_to_base(root_collection, objects_to_convert)
obj_id = operations.send(root_collection, [transport])
version_input = CreateVersionInput(
object_id=obj_id,
model_id=wm.selected_model_id,
project_id=wm.selected_project_id,
message=version_message,
source_application="blender",
)
version = client.version.create(version_input)
version_id = version.id
# track metrics
metrics.set_host_app("blender")
metrics.track(
metrics.SEND,
client.account,
{
"ui": "dui3",
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
"core_version": ".".join(map(str, bl_info["version"])),
"workspace_id": client.project.get(wm.selected_project_id).workspace_id,
},
)
# count total objects for success message
total_objects = count_objects_in_collection(root_collection)
return (
True,
f"Successfully published {total_objects} objects with hierarchy to Speckle",
version_id,
)
except Exception as e:
import traceback
traceback.print_exc()
# Clear cache on error to prevent stale clients
_client_cache.clear()
return False, f"Failed to publish: {str(e)}", None
def build_collection_hierarchy(
context: Context, objects_to_convert: List, apply_modifiers: bool = True
) -> Optional[Collection]:
"""
build a speckle collection hierarchy that mimicks blender's collection structure
"""
# set name for root collection
file_name = bpy.path.basename(bpy.data.filepath)
collection_name = file_name if file_name else "Untitled.blend"
collection_data = analyze_collection_structure(objects_to_convert)
if not collection_data["objects"] and not collection_data["collections"]:
return None
converted_objects = convert_selected_objects(
context, objects_to_convert, apply_modifiers
)
if not converted_objects:
return None
# create the root Speckle collection
root_collection = Collection(name=collection_name)
root_collection.units = get_scene_units(context.scene).value
root_collection["version"] = 3
# maps Blender collection to Speckle collection
collection_mapping = {} #
# create Speckle collections for each blender collection
for blender_coll in collection_data["collections"]:
speckle_coll = Collection(name=blender_coll.name)
speckle_coll.units = root_collection.units
collection_mapping[blender_coll] = speckle_coll
for blender_coll in collection_data["collections"]:
speckle_coll = collection_mapping[blender_coll]
parent_coll = find_parent_collection(
blender_coll, collection_data["collections"]
)
if parent_coll and parent_coll in collection_mapping:
parent_speckle_coll = collection_mapping[parent_coll]
parent_speckle_coll.elements.append(speckle_coll)
else:
root_collection.elements.append(speckle_coll)
# assign objects to their collections
object_mapping = {}
for i, blender_obj in enumerate(objects_to_convert):
if i < len(converted_objects) and converted_objects[i] is not None:
object_mapping[blender_obj] = converted_objects[i]
for blender_obj, speckle_obj in object_mapping.items():
placed = False
target_collection = find_target_collection_for_object(
blender_obj, collection_data["collections"]
)
if target_collection and target_collection in collection_mapping:
collection_mapping[target_collection].elements.append(speckle_obj)
placed = True
# if not placed in any subcollection, add to root
if not placed:
root_collection.elements.append(speckle_obj)
return root_collection
def analyze_collection_structure(objects: List) -> Dict:
"""
analyze the collection structure of the given objects
"""
collections_set = set()
objects_collections = {}
direct_collections = set()
for obj in objects:
obj_collections = []
for collection in bpy.data.collections:
if obj.name in collection.objects:
direct_collections.add(collection)
obj_collections.append(collection)
objects_collections[obj] = obj_collections
# find all ancestor collections
def find_all_ancestors(collection):
"""recursively find all ancestor collections"""
ancestors = set()
for potential_parent in bpy.data.collections:
if collection.name in potential_parent.children:
ancestors.add(potential_parent)
# Recursively find ancestors of the parent
ancestors.update(find_all_ancestors(potential_parent))
return ancestors
for collection in direct_collections:
collections_set.add(collection)
ancestors = find_all_ancestors(collection)
collections_set.update(ancestors)
collections_list = list(collections_set)
collections_list.sort(key=lambda c: get_collection_depth(c))
return {
"collections": collections_list,
"objects": objects,
"object_collections": objects_collections,
}
def get_collection_depth(collection: BlenderCollection) -> int:
"""
get the depth of a collection in the hierarchy
"""
depth = 0
for scene in bpy.data.scenes:
if collection.name in scene.collection.children:
return depth
for parent_coll in bpy.data.collections:
if collection.name in parent_coll.children:
return get_collection_depth(parent_coll) + 1
return depth
def find_parent_collection(
collection: BlenderCollection, all_collections: List[BlenderCollection]
) -> Optional[BlenderCollection]:
"""
find the parent collection
"""
for potential_parent in all_collections:
if collection.name in potential_parent.children:
return potential_parent
return None
def find_target_collection_for_object(
obj, collections: List[BlenderCollection]
) -> Optional[BlenderCollection]:
"""
find the deepest collection that contains this object
"""
target_collection = None
max_depth = -1
for collection in collections:
if obj.name in collection.objects:
depth = get_collection_depth(collection)
if depth > max_depth:
max_depth = depth
target_collection = collection
return target_collection
def convert_selected_objects(
context: Context, objects_to_convert: List, apply_modifiers: bool = True
) -> List[Optional[Base]]:
"""
convert selected objects to Speckle format with proper units
"""
scene = context.scene
units = get_scene_units(scene)
scale_factor = scene.unit_settings.scale_length
speckle_objects = []
for obj in objects_to_convert:
if not obj or obj.type not in ["MESH", "CURVE", "EMPTY"]:
speckle_objects.append(None)
continue
speckle_obj = convert_to_speckle(
obj, scale_factor, units.value, apply_modifiers
)
speckle_objects.append(speckle_obj)
return speckle_objects
def get_scene_units(scene) -> Units:
"""
get units from Blender's unit system
"""
unit_settings = scene.unit_settings
if unit_settings.system == "METRIC":
if unit_settings.length_unit == "METERS":
return Units.m
elif unit_settings.length_unit == "CENTIMETERS":
return Units.cm
elif unit_settings.length_unit == "MILLIMETERS":
return Units.mm
elif unit_settings.length_unit == "KILOMETERS":
return Units.km
else:
return Units.m
elif unit_settings.system == "IMPERIAL":
if unit_settings.length_unit == "FEET":
return Units.feet
elif unit_settings.length_unit == "INCHES":
return Units.inches
elif unit_settings.length_unit == "YARDS":
return Units.yards
elif unit_settings.length_unit == "MILES":
return Units.miles
else:
return Units.feet
else:
return Units.m # default to meters
def count_objects_in_collection(collection: Collection) -> int:
"""
recursively count all objects in a collection and its sub-collections
"""
count = 0
if hasattr(collection, "elements"):
for element in collection.elements:
if isinstance(element, Collection):
count += count_objects_in_collection(element)
else:
count += 1
return count
+16 -15
View File
@@ -1,30 +1,31 @@
import bpy
from bpy.props import CollectionProperty, StringProperty, IntProperty, IntVectorProperty
from bpy.props import CollectionProperty
from bpy.types import PropertyGroup
from typing import Optional
from ..utils.property_groups import speckle_model_card
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
manages the state of the Speckle addon in Blender
"""
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 get_model_card_by_id(self, model_card_id: str) -> Optional[speckle_model_card]:
"""Find a model card by its ID."""
for model_card in self.model_cards:
if model_card.get_model_card_id() == model_card_id:
return model_card
return None
def register() -> None:
bpy.utils.register_class(SpeckleState)
bpy.types.Scene.speckle_state = bpy.props.PointerProperty(type=SpeckleState) # type: ignore
def unregister() -> None:
del bpy.types.Scene.speckle_state
bpy.utils.unregister_class(SpeckleState)
+1 -1
View File
@@ -1 +1 @@
from .main_panel import SPECKLE_PT_main_panel # noqa: F401
from .main_panel import SPECKLE_PT_main_panel # noqa: F401
@@ -0,0 +1,143 @@
import bpy
from bpy.types import Context, Event
from typing import List, Tuple
from ..utils.account_manager import (
get_account_enum_items,
speckle_account,
speckle_workspace,
get_workspaces,
get_active_workspace,
get_account_from_id,
)
from ..utils.project_manager import get_projects_for_account
from ..ui.project_selection_dialog import speckle_project
class SPECKLE_UL_accounts_list(bpy.types.UIList):
"""
UIList for displaying accounts
"""
def draw_item(
self,
context: Context,
layout: bpy.types.UILayout,
data: bpy.types.PropertyGroup,
item: bpy.types.PropertyGroup,
icon: str,
active_data: bpy.types.PropertyGroup,
active_propname: str,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row()
row.label(text=item.user_name)
row.label(text=item.server_url)
row.label(text=item.user_email)
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.label(text=item.user_name)
class SPECKLE_OT_account_selection_dialog(bpy.types.Operator):
"""
operator for displaying and handling the account selection dialog
"""
bl_idname = "speckle.account_selection_dialog"
bl_label = "Select Account"
bl_description = "Select account"
account_index: bpy.props.IntProperty(default=0) # type: ignore
def invoke(self, context: Context, event: Event) -> set[str]:
wm = context.window_manager
# Clear existing accounts
wm.speckle_accounts.clear()
# Save selected account
current_account_index = 0
# Fetch accounts
for i, (id, user_name, server_url, user_email) in enumerate(
get_account_enum_items()
):
account: speckle_account = wm.speckle_accounts.add()
account.id = id
account.user_name = user_name
account.server_url = server_url
account.user_email = user_email
if id == wm.selected_account_id:
current_account_index = i
self.account_index = current_account_index
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout = self.layout
wm = context.window_manager
row = layout.row()
# add account button
if wm.selected_account_id == "NO_ACCOUNTS":
add_account_button_text = "Sign In"
add_account_button_icon = "WORLD"
else:
add_account_button_text = "Add Account"
add_account_button_icon = "ADD"
row.operator(
"speckle.add_account",
icon=add_account_button_icon,
text=add_account_button_text,
)
if wm.selected_account_id != "NO_ACCOUNTS":
layout.template_list(
"SPECKLE_UL_accounts_list",
"",
context.window_manager,
"speckle_accounts",
self,
"account_index",
)
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
# update the selected account id
account = get_account_from_id(wm.speckle_accounts[self.account_index].id)
wm.selected_account_id = account.id
self.report(
{"INFO"},
f"Selected account: {account.userInfo.name} - {account.userInfo.email} - {account.serverInfo.url}",
)
update_workspaces_list(context)
update_projects_list(context)
# redraw the area
context.area.tag_redraw()
return {"FINISHED"}
def update_workspaces_list(context: Context) -> None:
wm = context.window_manager
wm.speckle_workspaces.clear()
workspaces = get_workspaces(wm.selected_account_id)
for id, name in workspaces:
workspace: speckle_workspace = wm.speckle_workspaces.add()
workspace.id = id
workspace.name = name
wm.selected_workspace.id = get_active_workspace(wm.selected_account_id)["id"]
print("Updated Workspaces List!")
def update_projects_list(context: Context) -> None:
wm = context.window_manager
wm.speckle_projects.clear()
projects: List[Tuple[str, str, str, str, bool]] = get_projects_for_account(
wm.selected_account_id, workspace_id=wm.selected_workspace.id
)
for name, role, updated, id, can_receive in projects:
project: speckle_project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
project.can_receive = can_receive
print("Updated Projects List!")
+14 -2
View File
@@ -4,19 +4,31 @@ 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')
speckle_logo_icon_path = os.path.join(icons_dir, "speckle-logo.png")
if os.path.exists(speckle_logo_icon_path):
speckle_icons.load("speckle_logo", speckle_logo_icon_path, "IMAGE")
else:
print(f"[Speckle] WARNING icon file not found: {speckle_logo_icon_path}")
object_highlight_icon_path = os.path.join(icons_dir, "object-highlight.png")
if os.path.exists(object_highlight_icon_path):
speckle_icons.load("object_highlight", object_highlight_icon_path, "IMAGE")
else:
print(f"[Speckle] WARNING icon file not found: {object_highlight_icon_path}")
def unload_icons() -> None:
global speckle_icons
if speckle_icons is not None:
bpy.utils.previews.remove(speckle_icons)
def get_icon(icon_name: str) -> int:
global speckle_icons
if speckle_icons is None:
raise ValueError("Icons not loaded")
return speckle_icons[icon_name].icon_id
return speckle_icons[icon_name].icon_id
+142 -57
View File
@@ -1,88 +1,173 @@
"""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.
"""
main panel for the Speckle addon.
"""
bl_label = "Speckle"
bl_idname = "SPECKLE_PT_main_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Speckle'
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"))
layout.label(text="Speckle Connector", icon_value=get_icon("speckle_logo"))
# Check to see if there are any speckle models in the file
# 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')
wm = context.window_manager
project_selected = bool(getattr(wm, "selected_project_name", None))
model_selected = bool(getattr(wm, "selected_model_name", None))
version_selected = bool(getattr(wm, "selected_version_id", None))
selection_made = bool(getattr(wm, "speckle_objects", None))
# UI Mode Switch
row = layout.row()
row.prop(wm, "ui_mode", expand=True)
# select Project button
row = layout.row()
project_name = getattr(wm, "selected_project_name", "")
project_button_text = project_name if project_selected else "Select Project"
project_button_icon = "CHECKMARK" if project_selected else "PLUS"
row.operator(
"speckle.project_selection_dialog",
text=project_button_text,
icon=project_button_icon,
)
# select Model button
row = layout.row()
model_name = getattr(wm, "selected_model_name", "")
model_button_text = model_name if model_selected else "Select Model"
model_button_icon = "CHECKMARK" if model_selected else "PLUS"
row.enabled = project_selected
row.operator(
"speckle.model_selection_dialog",
text=model_button_text,
icon=model_button_icon,
)
if wm.ui_mode == "PUBLISH":
# TODO: implement Publish flow
# Selection filter
row = layout.row()
row.enabled = project_selected and model_selected
selection_button_text = (
f"{len(wm.speckle_objects)} Objects"
if wm.speckle_objects
else "Select Objects"
)
row.operator(
"speckle.selection_filter_dialog",
text=selection_button_text,
icon="PLUS",
).model_card_id = ""
# Publish button
row = layout.row()
row.enabled = project_selected and model_selected and selection_made
row.operator("speckle.publish", text="Publish Model", icon="EXPORT")
pass
if wm.ui_mode == "LOAD":
# select Version button
row = layout.row()
version_id = getattr(wm, "selected_version_id", "")
load_option = getattr(wm, "selected_version_load_option", "")
if load_option == "LATEST":
version_button_text = "Latest"
elif load_option == "SPECIFIC":
version_button_text = version_id
else:
version_button_text = "Select Version"
version_button_icon = "CHECKMARK" if version_selected else "PLUS"
row.enabled = project_selected and model_selected
row.operator(
"speckle.version_selection_dialog",
text=version_button_text,
icon=version_button_icon,
).model_card_id = ""
# load button
row = layout.row()
row.enabled = project_selected and model_selected and version_selected
row.operator("speckle.load", text="Load Model", icon="IMPORT")
layout.separator()
# Group model cards by project name
# 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"
for model_card in context.scene.speckle_state.model_cards:
project_name = (
model_card.project_name if model_card.project_name else "No Project"
)
if project_name not in project_groups:
project_groups[project_name] = []
project_groups[project_name].append((model_card_index, model_card))
# Render model cards grouped by project
project_groups[project_name].append(model_card)
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:
project_row.label(text=f"Project: {project_name}", icon="TRIA_RIGHT")
for 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
row_1: UILayout = box.row()
row_2: UILayout = box.row()
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}")
# Publish button in the model card
row_1.operator(
"speckle.model_card_publish", text="", icon="EXPORT"
).model_card_id = model_card.get_model_card_id()
# Selection filter button in the model card
row_2.operator(
"speckle.selection_filter_dialog",
text=f"Selection: {len(model_card.objects)} objects",
).model_card_id = model_card.get_model_card_id()
elif not model_card.is_publish:
# Load button in the model card
row_1.operator(
"speckle.model_card_load", text="", icon="IMPORT"
).model_card_id = model_card.get_model_card_id()
version_button_text = (
f"Latest: {model_card.version_id}"
if model_card.load_option == "LATEST"
else f"{model_card.version_id}"
)
row_2.operator(
"speckle.version_selection_dialog",
text=version_button_text,
).model_card_id = model_card.get_model_card_id()
# TODO: Get last updated time
split.label(text="Last updated: 2 days ago")
else:
print({"ERROR"}, "Model card state unknown")
return
row_1.label(text=f"{model_card.model_name}")
# Select button in the model card
select_op = row_1.operator(
"speckle.select_objects",
text="",
icon_value=get_icon("object_highlight"),
)
select_op.model_card_id = model_card.get_model_card_id()
# Settings button in the model card
row_1.operator(
"speckle.model_card_settings", text="", icon="COLLAPSEMENU"
).model_card_id = model_card.get_model_card_id()
-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,51 +1,26 @@
"""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 bpy.types import Context, Event, PropertyGroup, UILayout
from ..utils.model_manager import get_models_for_project
from ..utils.version_manager import get_latest_version
class speckle_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.
"""
UIList for displaying a list of Speckle models
"""
def draw_item(self, context: Context, layout: UILayout, data: PropertyGroup, item: PropertyGroup,
icon: str, active_data: PropertyGroup, active_propname: str) -> None:
"""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'}:
def draw_item(
self,
context: Context,
layout: UILayout,
data: PropertyGroup,
item: PropertyGroup,
icon: str,
active_data: PropertyGroup,
active_propname: str,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
split = row.split(factor=0.5)
split.label(text=item.name)
@@ -53,127 +28,92 @@ class SPECKLE_UL_models_list(bpy.types.UIList):
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'
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.
class SPECKLE_OT_model_selection_dialog(bpy.types.Operator):
"""
operator for displaying and handling the model selection dialog
"""
bl_idname = "speckle.model_selection_dialog"
bl_label = "Select Model"
bl_description = "Select a model to load"
def update_models_list(self, context: Context) -> None:
"""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
models = get_models_for_project(
wm.selected_account_id, wm.selected_project_id, search=search
)
for name, id, updated in models:
model = wm.speckle_models.add()
model.name = name
model.updated = updated
model.id = id
return None
search_query: bpy.props.StringProperty( # type: ignore
name="Search",
description="Search a model",
default="",
update=update_models_list
)
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=""
update=update_models_list,
)
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'}
wm = context.window_manager
if 0 <= self.model_index < len(wm.speckle_models):
selected_model = wm.speckle_models[self.model_index]
wm.selected_model_id = selected_model.id
wm.selected_model_name = selected_model.name
latest_version = get_latest_version(
account_id=wm.selected_account_id,
project_id=wm.selected_project_id,
model_id=wm.selected_model_id,
)
if latest_version:
wm.selected_version_load_option = "LATEST"
wm.selected_version_id = latest_version[0]
print(f"Selected model: {selected_model.name} ({selected_model.id})")
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
# 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
layout: UILayout = self.layout
wm = context.window_manager
layout.label(text=f"Project: {wm.selected_project_name}")
row = layout.row(align=True)
row.prop(self, "search_query", icon='VIEWZOOM', text="")
# Models UIList
layout.template_list("SPECKLE_UL_models_list", "", context.window_manager, "speckle_models", self, "model_index")
row.prop(self, "search_query", icon="VIEWZOOM", text="") # search bar
if wm.ui_mode != "LOAD":
row.operator("speckle.create_model", icon="ADD", text="")
layout.template_list(
"SPECKLE_UL_models_list",
"",
context.window_manager,
"speckle_models",
self,
"model_index",
)
layout.separator()
# 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])
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

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

Before

Width:  |  Height:  |  Size: 445 B

After

Width:  |  Height:  |  Size: 446 B

@@ -1,41 +1,11 @@
"""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 bpy.types import UILayout, Context, PropertyGroup, Event
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.
"""
UIList for displaying a list of Speckle versions
"""
# TODO: Adjust column widths so message has the most space.
@@ -49,18 +19,6 @@ class SPECKLE_UL_versions_list(bpy.types.UIList):
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)
@@ -68,49 +26,16 @@ class SPECKLE_UL_versions_list(bpy.types.UIList):
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.
"""
class SPECKLE_OT_version_selection_dialog(bpy.types.Operator):
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=""
)
bl_description = "Select a model version to load. Default is the latest version. You can also select a specific version."
version_index: bpy.props.IntProperty(name="Model Index", default=0) # type: ignore
@@ -128,21 +53,22 @@ class SPECKLE_OT_version_selection_dialog(MousePositionMixin, bpy.types.Operator
default="LATEST",
)
model_card_id: bpy.props.StringProperty(
name="Model Card ID",
description="This is used to indicate the function is called from a model card",
default="",
) # type: ignore
def update_versions_list(self, context: Context) -> None:
wm = context.window_manager
# 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,
project_id=wm.selected_project_id,
model_id=wm.selected_model_id,
)
# Populate versions list
for id, message, updated in versions:
version = wm.speckle_versions.add()
version.id = id
@@ -153,64 +79,99 @@ class SPECKLE_OT_version_selection_dialog(MousePositionMixin, bpy.types.Operator
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)
version_id_to_store = ""
if self.load_option == "LATEST":
latest_version = get_latest_version(
account_id=wm.selected_account_id,
project_id=wm.selected_project_id,
model_id=wm.selected_model_id,
)
if latest_version:
version_id_to_store = latest_version[0]
else:
print(
f"Could not fetch latest version for model {wm.selected_model_id}"
)
return {"CANCELLED"}
elif self.load_option == "SPECIFIC":
if 0 <= self.version_index < len(wm.speckle_versions):
selected_version = wm.speckle_versions[self.version_index]
version_id_to_store = selected_version.id
else:
print(f"Invalid version index {self.version_index}")
return {"CANCELLED"}
wm.selected_version_id = version_id_to_store
if self.model_card_id != "":
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
if model_card is None:
self.report({"ERROR"}, f"Model card '{self.model_card_id}' not found")
return {"CANCELLED"}
model_card.load_option = self.load_option
model_card.version_id = version_id_to_store
self.report(
{"INFO"},
f"Model card updated: Selected version: {model_card.version_id}, Option: {self.load_option}",
)
bpy.ops.speckle.model_card_load(model_card_id=self.model_card_id)
context.area.tag_redraw()
return {"FINISHED"}
wm.selected_version_load_option = self.load_option
self.report(
{"INFO"},
f"Selected version: {version_id_to_store} (Option: {self.load_option})",
)
context.area.tag_redraw()
return {"FINISHED"}
def invoke(self, context: Context, event: Event) -> set[str]:
# 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
if self.model_card_id != "":
wm = context.window_manager
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
self.load_option = model_card.load_option
wm.selected_account_id = model_card.account_id
wm.selected_project_id = model_card.project_id
wm.selected_model_id = model_card.model_id
wm.selected_version_id = model_card.version_id
# 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}")
wm = context.window_manager
project_name = wm.selected_project_name
model_name = wm.selected_model_name
if self.model_card_id != "":
model_card = context.scene.speckle_state.get_model_card_by_id(
self.model_card_id
)
project_name = model_card.project_name
model_name = model_card.model_name
# Radio buttons for load options
layout.prop(self, "load_option", expand=True)
layout.label(text=f"Project: {project_name}")
layout.label(text=f"Model: {model_name}")
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",
@@ -222,6 +183,3 @@ class SPECKLE_OT_version_selection_dialog(MousePositionMixin, bpy.types.Operator
)
layout.separator()
# Restore mouse position
self.restore_mouse_position(context)
@@ -0,0 +1,108 @@
import bpy
from bpy.types import Context, UILayout, Event, PropertyGroup
from typing import List, Tuple
from ..utils.account_manager import get_workspaces, speckle_workspace
from ..utils.project_manager import get_projects_for_account
from ..utils.account_manager import can_create_project_in_workspace
class SPECKLE_UL_workspaces_list(bpy.types.UIList):
"""
UIList for workspaces
"""
def draw_item(
self,
context: Context,
layout: UILayout,
data: PropertyGroup,
item: PropertyGroup,
icon: str,
active_data: PropertyGroup,
active_propname: str,
) -> None:
if self.layout_type in {"DEFAULT", "COMPACT"}:
row = layout.row(align=True)
row.label(text=item.name)
elif self.layout_type == "GRID":
layout.alignment = "CENTER"
layout.label(text=item.name)
class SPECKLE_OT_workspace_selection_dialog(bpy.types.Operator):
"""
Operator for selecting a workspace
"""
bl_idname = "speckle.workspace_selection_dialog"
bl_label = "Select Workspace"
bl_description = "Select a workspace to load projects from"
workspace_index: bpy.props.IntProperty(name="Workspace Index", default=0) # type: ignore
def invoke(self, context: Context, event: Event) -> set[str]:
wm = context.window_manager
wm.speckle_workspaces.clear()
workspaces: List[Tuple[str, str]] = get_workspaces(wm.selected_account_id)
current_workspace_index = 0
for i, (id, name) in enumerate(workspaces):
workspace: speckle_workspace = wm.speckle_workspaces.add()
workspace.id = id
workspace.name = name
if id == wm.selected_workspace.id:
current_workspace_index = i
self.workspace_index = current_workspace_index
return context.window_manager.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
wm = context.window_manager
layout.label(text=f"Selected Workspace: {wm.selected_workspace.name}")
layout.template_list(
"SPECKLE_UL_workspaces_list",
"",
context.window_manager,
"speckle_workspaces",
self,
"workspace_index",
)
def execute(self, context: Context) -> set[str]:
wm = context.window_manager
if 0 <= self.workspace_index < len(wm.speckle_workspaces):
selected_workspace = wm.speckle_workspaces[self.workspace_index]
wm.selected_workspace.id = selected_workspace.id
wm.selected_workspace.name = selected_workspace.name
update_projects_list(context)
context.area.tag_redraw()
return {"FINISHED"}
def update_projects_list(context):
"""Update projects list when workspace changes"""
wm = context.window_manager
wm.speckle_projects.clear()
# get projects for the selected account and workspace
projects = get_projects_for_account(
wm.selected_account_id, wm.selected_workspace.id
)
for name, role, updated, id, can_receive in projects:
project = wm.speckle_projects.add()
project.name = name
project.role = role
project.updated = updated
project.id = id
project.can_receive = can_receive
# Update can_create_project_in_workspace flag
wm.can_create_project_in_workspace = can_create_project_in_workspace(
wm.selected_account_id, wm.selected_workspace.id
)
print(f"Workspace changed to: {wm.selected_workspace.id}")
print("Projects list updated")
context.area.tag_redraw()
+287 -33
View File
@@ -1,53 +1,307 @@
from specklepy.api.credentials import get_local_accounts
from typing import List, Tuple, Optional
from specklepy.core.api.credentials import Account
from typing import Dict, List, Optional, Tuple
import bpy
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account, get_local_accounts
from specklepy.core.api.wrapper import StreamWrapper
from .misc import strip_non_ascii
def get_account_enum_items() -> List[Tuple[str, str, str]]:
"""Retrieves a list of Speckle accounts formatted for Blender enum properties.
class SpeckleClientCache:
def __init__(self):
self._clients: Dict[str, SpeckleClient] = {}
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.
def get_client(self, account_id: str) -> SpeckleClient:
# Check cache first
if account_id in self._clients:
print(f"[Cache HIT] Using cached client for account {account_id}")
return self._clients[account_id]
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
# Create new client if needed
print(f"[Cache MISS] Creating new client for account {account_id}")
account = get_account_from_id(account_id)
if not account:
raise ValueError(f"No account found for ID: {account_id}")
Note:
If no accounts are found, returns a single tuple with instructions for adding an account.
assert account.serverInfo.url
client = SpeckleClient(
host=account.serverInfo.url,
use_ssl=account.serverInfo.url.startswith("https"),
)
client.authenticate_with_account(account)
self._clients[account_id] = client
return client
def clear(self) -> None:
"""Clear all cached clients."""
print("[Cache] Clearing all cached clients")
self._clients.clear()
# Global cache instance
_client_cache = SpeckleClientCache()
class speckle_account(bpy.types.PropertyGroup):
id: bpy.props.StringProperty() # type: ignore
user_name: bpy.props.StringProperty() # type: ignore
server_url: bpy.props.StringProperty() # type: ignore
user_email: bpy.props.StringProperty() # type: ignore
class speckle_workspace(bpy.types.PropertyGroup):
"""
PropertyGroup for storing workspace information
"""
id: bpy.props.StringProperty(name="ID") # type: ignore
name: bpy.props.StringProperty() # type: ignore
def get_account_enum_items() -> List[Tuple[str, str, str, str]]:
accounts: List[Account] = get_local_accounts()
if not accounts:
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]
print("No accounts found!")
return [("NO_ACCOUNTS", "No accounts found!", "", "")]
print("Accounts added")
speckle_accounts = []
for acc in accounts:
speckle_accounts.append(
(
acc.id,
strip_non_ascii(acc.userInfo.name),
acc.serverInfo.url,
acc.userInfo.email,
)
)
return speckle_accounts
def get_workspaces(account_id: str) -> List[Tuple[str, str]]:
"""
retrieves the workspaces for a given account ID
"""
try:
# Get client from cache
client = _client_cache.get_client(account_id)
workspaces_enabled = client.server.get().workspaces.workspaces_enabled
if workspaces_enabled:
workspaces = client.active_user.get_workspaces().items
workspace_list = [
(ws.id, strip_non_ascii(ws.name))
for ws in workspaces
if ws.creation_state is None or ws.creation_state.completed
]
personal_projects_text = "Personal Projects (Legacy)"
else:
workspace_list = []
personal_projects_text = "Personal Projects"
workspace_list.append(("personal", personal_projects_text))
if workspaces_enabled:
active_workspace = client.active_user.get_active_workspace()
default_workspace_id = (
active_workspace.id if active_workspace else "personal"
)
result = reorder_tuple(workspace_list, default_workspace_id)
else:
result = workspace_list
return result
except Exception as e:
print(f"Error in get_workspaces: {str(e)}")
_client_cache.clear() # Clear cache on error
return [("", "")]
def get_default_account_id() -> Optional[str]:
"""Retrieves the ID of the default Speckle account.
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)
retrieves the ID of the default Speckle account
"""
return next(
(acc.id for acc in get_local_accounts() if acc.isDefault), "NO_ACCOUNTS"
)
def get_server_url_by_account_id(account_id: str) -> Optional[str]:
"""Retrieves the server URL for a given account ID.
Args:
account_id (str): The ID of the account.
Returns:
Optional[str]: The server URL if the account is found, otherwise None.
"""
retrieves the server URL for a given account ID
"""
accounts: List[Account] = get_local_accounts()
for acc in accounts:
if acc.id == account_id:
return acc.serverInfo.url
return None
return None
def get_active_workspace(account_id: str) -> Optional[Dict[str, str]]:
"""
retrieves the ID of the default workspace for a given account ID
"""
try:
client = _client_cache.get_client(account_id)
active_workspace = client.active_user.get_active_workspace()
if active_workspace:
return {"id": active_workspace.id, "name": active_workspace.name}
return {"id": "personal", "name": "Personal Projects"}
except Exception as e:
print(f"Error in get_active_workspace: {str(e)}")
_client_cache.clear()
return None
def get_account_from_id(account_id: str) -> Optional[Account]:
return next((acc for acc in get_local_accounts() if acc.id == account_id), None)
def reorder_tuple(tuple_list, target_id):
for i, (id, value) in enumerate(tuple_list):
if id == target_id:
# Remove the tuple from its current position
target_tuple = tuple_list.pop(i)
# Insert it at the beginning of the list
tuple_list.insert(0, target_tuple)
return tuple_list
# If the target_id wasn't found
print(f"Tuple with ID {target_id} not found in the list")
return tuple_list
def get_project_from_url(
url: str,
) -> Tuple[Optional[StreamWrapper], Optional[object], Optional[object], str]:
"""
get a project from a URL, handling all the client setup.
"""
try:
wrapper = StreamWrapper(url)
account = wrapper.get_account()
assert account.id
client = _client_cache.get_client(account.id)
# get the stream_id (project_id) from the wrapper
if not wrapper.stream_id:
return wrapper, client, None, "Could not extract project ID from URL"
project = client.project.get(wrapper.stream_id)
if not project:
return wrapper, client, None, "Could not access project"
return wrapper, client, project, ""
except Exception as e:
return None, None, None, f"Failed to process URL: {str(e)}"
def get_model_details_by_wrapper(
wrapper: StreamWrapper,
) -> Tuple[str, str, str, str, str, str, str]:
"""
extract model details from a StreamWrapper object.
"""
client = wrapper.get_client()
client.authenticate_with_account(wrapper.get_account())
(
account_id,
project_id,
project_name,
model_id,
model_name,
version_id,
load_option,
) = "", "", "", "", "", "", ""
account_id = wrapper.get_account().id
if wrapper.stream_id:
project_id = wrapper.stream_id
project_name = client.project.get(project_id).name
if wrapper.model_id:
model_id = wrapper.model_id
model = client.model.get(model_id, project_id)
model_name = model.name
load_option = "LATEST" if not wrapper.commit_id else "SPECIFIC"
if wrapper.commit_id:
version_id = wrapper.commit_id
else:
versions = client.version.get_versions(
wrapper.model_id, wrapper.stream_id, limit=1
)
if versions.items and len(versions.items) > 0:
version_id = versions.items[0].id
else:
version_id = ""
return (
account_id,
project_id,
project_name,
model_id,
model_name,
version_id,
load_option,
)
def can_load(client, project) -> Tuple[bool, str]:
try:
permissions = client.project.get_permissions(project.id)
if permissions.can_load.authorized:
return True, ""
else:
return (
False,
"Your role on this project doesn't give you permission to load.",
)
except Exception as e:
error_msg = f"Failed to check permissions: {str(e)}"
print(error_msg)
return False, error_msg
def can_publish(client, project) -> Tuple[bool, str]:
try:
permissions = client.project.get_permissions(project.id)
if permissions.can_publish.authorized:
return True, ""
else:
return (
False,
"Your role on this project doesn't give you permission to publish.",
)
except Exception as e:
error_msg = f"Failed to check permissions: {str(e)}"
print(error_msg)
return False, error_msg
def can_create_project_in_workspace(account_id: str, workspace_id: str) -> bool:
"""
Check if the user can create a project in the specified workspace.
"""
try:
client = _client_cache.get_client(account_id)
if workspace_id == "personal":
return client.active_user.can_create_personal_projects().authorized
else:
try:
workspace = client.workspace.get(workspace_id)
return workspace.permissions.can_create_project.authorized
except Exception as e:
print(f"Failed to get workspace: {str(e)}")
return False
except Exception as e:
print(f"Error in can_create_project_in_workspace: {str(e)}")
_client_cache.clear() # Clear cache on error
return False
@@ -5,7 +5,7 @@ from specklepy.objects.graph_traversal.traversal import TraversalContext
def get_ascendants(context: TraversalContext) -> Iterator[Base]:
"""
Walks up the tree, returning all ascendants, including context
walks up the tree, returning all ascendants, including context
"""
head = context
while head is not None:
@@ -18,7 +18,7 @@ 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,
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):
+21 -37
View File
@@ -1,47 +1,31 @@
from datetime import datetime, timezone
import re
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.
convert UTC timestamp to local timezone and return relative time string
"""
if not timestamp:
return "Unknown"
# Convert to local timezone
# convert to local timezone
try:
# First try parsing as ISO format
try:
dt = datetime.fromisoformat(str(timestamp).replace('Z', '+00:00'))
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)
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
local_dt = dt.astimezone()
# calculate relative time
now = datetime.now(timezone.utc).astimezone()
delta = now - local_dt
if delta.days == 0:
if delta.seconds < 3600:
minutes = delta.seconds // 60
@@ -54,16 +38,16 @@ def format_relative_time(timestamp) -> str:
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)
returns just the role part
"""
split_role = role.split(":")
return f"{split_role[1]}"
return f"{split_role[1]}"
def strip_non_ascii(text):
# Keep English letters, digits, spaces and basic punctuation
return re.sub(r"[^a-zA-Z0-9\s.,!?]", "", text)
@@ -0,0 +1,418 @@
import bpy
from bpy.types import Context
from typing import Dict, Any, Optional
from ..utils.property_groups import speckle_model_card
def find_layer_collection(layer_collection, collection_name):
"""
Recursively find a layer collection by collection name
"""
if layer_collection.collection.name == collection_name:
return layer_collection
for child in layer_collection.children:
result = find_layer_collection(child, collection_name)
if result:
return result
return None
def get_object_by_application_id(app_id: str):
"""
Find a Blender object by its applicationId stored in custom property
"""
if not app_id:
return None
for obj in bpy.data.objects:
if "applicationId" in obj and obj["applicationId"] == app_id:
return obj
return None
def get_objects_by_application_ids(app_ids: list):
"""
Find multiple Blender objects by their applicationIds
"""
if not app_ids:
return {}
result = {}
for obj in bpy.data.objects:
if "applicationId" in obj and obj["applicationId"] in app_ids:
result[obj["applicationId"]] = obj
return result
def get_collection_by_application_id(app_id: str):
"""
Find a Blender collection by its applicationId stored in custom property
"""
if not app_id:
return None
for collection in bpy.data.collections:
if "applicationId" in collection and collection["applicationId"] == app_id:
return collection
return None
def get_collection_identifier(blender_col: bpy.types.Collection) -> str:
"""
Get collection identifier: applicationId if exists, fallback to name
"""
if "applicationId" in blender_col and blender_col["applicationId"]:
return blender_col["applicationId"]
return blender_col.name
def find_collection_by_identifier(identifier: str):
"""
Find collection by identifier: try applicationId first, then name
"""
# first try to find by applicationId
collection = get_collection_by_application_id(identifier)
if collection:
return collection
# fallback to name-based lookup
return bpy.data.collections.get(identifier)
def capture_modifier_data(blender_obj: bpy.types.Object) -> list:
"""
Capture modifier data from a Blender object as dictionaries
"""
modifiers_data = []
for modifier in blender_obj.modifiers:
modifier_data = {
"name": modifier.name,
"type": modifier.type,
"show_viewport": modifier.show_viewport,
"show_render": modifier.show_render,
"show_in_editmode": modifier.show_in_editmode,
"show_on_cage": modifier.show_on_cage,
"properties": {},
}
# Capture modifier-specific properties
for prop_name in modifier.bl_rna.properties.keys():
if prop_name in [
"rna_type",
"name",
"type",
"show_viewport",
"show_render",
"show_in_editmode",
"show_on_cage",
]:
continue
try:
if hasattr(modifier, prop_name):
prop_value = getattr(modifier, prop_name)
# Handle different property types
if isinstance(prop_value, (int, float, bool, str)):
modifier_data["properties"][prop_name] = prop_value
elif hasattr(prop_value, "name"): # Object references
modifier_data["properties"][prop_name] = prop_value.name
elif (
hasattr(prop_value, "__len__") and len(prop_value) <= 4
): # Vectors/colors
modifier_data["properties"][prop_name] = list(prop_value)
except (AttributeError, TypeError):
continue
modifiers_data.append(modifier_data)
return modifiers_data
def has_visibility_modifications(obj: bpy.types.Object) -> bool:
"""Check if object has non-default visibility settings"""
return obj.hide_viewport or obj.hide_select or obj.hide_render or obj.hide_get()
def has_modifier_modifications(obj: bpy.types.Object) -> bool:
"""Check if object has any modifiers applied"""
return hasattr(obj, "modifiers") and len(obj.modifiers) > 0
def has_collection_visibility_modifications(layer_col, collection) -> bool:
"""Check if collection has non-default visibility settings"""
return (
layer_col.hide_viewport
or collection.hide_select
or collection.hide_render
or layer_col.exclude
)
def collect_objects_with_properties(
model_card: speckle_model_card,
) -> Dict[str, Dict[str, Any]]:
"""
Collect objects and collections with their current properties before deletion
Only stores data for objects that have been modified from defaults
"""
collected_data = {"objects": {}, "collections": {}}
# Collect object properties (only for modified objects)
for s_obj in model_card.objects:
blender_obj = get_object_by_application_id(s_obj.applicationId)
if blender_obj:
obj_data = {}
# Only collect visibility if modified from defaults
if has_visibility_modifications(blender_obj):
obj_data["visibility"] = {
"hide_get": blender_obj.hide_get(),
"hide_viewport": blender_obj.hide_viewport,
"hide_select": blender_obj.hide_select,
"hide_render": blender_obj.hide_render,
}
# Only collect modifiers if object has any
if has_modifier_modifications(blender_obj):
obj_data["modifiers"] = capture_modifier_data(blender_obj)
# Only store object data if it has modifications
if obj_data:
collected_data["objects"][s_obj.applicationId] = obj_data
# Collect collection properties (only for modified collections)
for s_col in model_card.collections:
# try to find collection by applicationId first, then fallback to name
blender_col = None
if s_col.applicationId:
blender_col = get_collection_by_application_id(s_col.applicationId)
if not blender_col:
blender_col = bpy.data.collections.get(s_col.name)
if blender_col:
view_layer = bpy.context.view_layer
if view_layer:
layer_col = find_layer_collection(
view_layer.layer_collection, blender_col.name
)
if layer_col and has_collection_visibility_modifications(
layer_col, blender_col
):
# use collection identifier as key
collection_id = get_collection_identifier(blender_col)
collected_data["collections"][collection_id] = {
"hide_viewport": layer_col.hide_viewport,
"hide_select": layer_col.collection.hide_select,
"hide_render": layer_col.collection.hide_render,
"exclude_from_view_layer": layer_col.exclude,
}
return collected_data
def transfer_object_properties(
new_obj: bpy.types.Object, old_obj_data: Dict[str, Any]
) -> None:
"""
Transfer visibility and modifiers from old object data to new object
Handles sparse data gracefully - applies defaults when data is missing
"""
# Transfer visibility settings (if any were modified)
visibility = old_obj_data.get("visibility")
if visibility:
new_obj.hide_set(visibility.get("hide_get", False))
new_obj.hide_viewport = visibility.get("hide_viewport", False)
new_obj.hide_select = visibility.get("hide_select", False)
new_obj.hide_render = visibility.get("hide_render", False)
# If no visibility data, object keeps defaults (all False)
# Transfer modifiers (if any were present)
old_modifiers = old_obj_data.get("modifiers")
if old_modifiers and hasattr(new_obj, "modifiers"):
# Clear existing modifiers
new_obj.modifiers.clear()
# Transfer each modifier
for modifier_data in old_modifiers:
recreate_modifier_from_data(new_obj, modifier_data)
# If no modifier data, object keeps default (no modifiers)
def transfer_collection_properties(
new_col: bpy.types.Collection, old_col_data: Dict[str, Any]
) -> None:
"""
Transfer visibility properties from old collection data to new collection
Handles sparse data gracefully - applies defaults when data is missing
"""
view_layer = bpy.context.view_layer
if view_layer:
layer_col = find_layer_collection(view_layer.layer_collection, new_col.name)
if layer_col:
# Only apply properties if collection had modifications
# (otherwise it keeps defaults: all False)
layer_col.hide_viewport = old_col_data.get("hide_viewport", False)
layer_col.collection.hide_select = old_col_data.get("hide_select", False)
layer_col.collection.hide_render = old_col_data.get("hide_render", False)
layer_col.exclude = old_col_data.get("exclude_from_view_layer", False)
def recreate_modifier_from_data(
new_obj: bpy.types.Object, modifier_data: Dict[str, Any]
) -> Optional[bpy.types.Modifier]:
"""
Recreate a modifier from captured data
"""
try:
# Validate modifier data
if not modifier_data.get("type") or not modifier_data.get("name"):
print(f"Invalid modifier data: {modifier_data}")
return None
# Create new modifier
new_modifier = new_obj.modifiers.new(
modifier_data["name"], modifier_data["type"]
)
# Set visibility properties
new_modifier.show_viewport = modifier_data.get("show_viewport", True)
new_modifier.show_render = modifier_data.get("show_render", True)
new_modifier.show_in_editmode = modifier_data.get("show_in_editmode", True)
new_modifier.show_on_cage = modifier_data.get("show_on_cage", False)
# Set modifier-specific properties
for prop_name, prop_value in modifier_data.get("properties", {}).items():
try:
if hasattr(new_modifier, prop_name):
current_value = getattr(new_modifier, prop_name)
# Handle object references
if hasattr(current_value, "name") and isinstance(prop_value, str):
referenced_obj = bpy.data.objects.get(prop_value)
if referenced_obj:
setattr(new_modifier, prop_name, referenced_obj)
else:
setattr(new_modifier, prop_name, prop_value)
except (AttributeError, TypeError):
continue
return new_modifier
except Exception as e:
print(f"Error recreating modifier {modifier_data.get('name', 'unknown')}: {e}")
return None
def update_model_card_objects(
model_card: speckle_model_card,
converted_objects: Dict[str, bpy.types.Object | bpy.types.Collection],
old_properties: Optional[Dict[str, Dict[str, Any]]] = None,
):
"""
Update model card with new objects and apply properties from old objects if provided
"""
# Clear model card objects
model_card.objects.clear()
model_card.collections.clear()
# Convert list to dictionary if needed
if isinstance(converted_objects, list):
converted_objects = {obj.name: obj for obj in converted_objects}
for obj in converted_objects.values():
# Handle collections
if isinstance(obj, bpy.types.Collection):
if obj.name in (o.name for o in model_card.collections):
continue
s_col = model_card.collections.add()
s_col.name = obj.name
s_col.applicationId = obj.get("applicationId", "")
# apply old collection properties if available (use identifier-based lookup)
if old_properties:
collection_id = get_collection_identifier(obj)
if collection_id in old_properties.get("collections", {}):
old_col_data = old_properties["collections"][collection_id]
transfer_collection_properties(obj, old_col_data)
# Handle objects
elif isinstance(obj, bpy.types.Object):
if obj.name in (o.name for o in model_card.objects):
continue
s_obj = model_card.objects.add()
s_obj.name = obj.name
s_obj.applicationId = obj.get("applicationId", "")
# Apply old object properties if available
if (
old_properties
and s_obj.applicationId
and s_obj.applicationId in old_properties.get("objects", {})
):
old_obj_data = old_properties["objects"][s_obj.applicationId]
transfer_object_properties(obj, old_obj_data)
def delete_model_card_objects(model_card: speckle_model_card, context: Context) -> None:
"""
deletes the model card objects
"""
# Delete objects directly without requiring selection
for obj in model_card.objects:
blender_obj = get_object_by_application_id(obj.applicationId)
if not blender_obj:
continue
# Remove object from all collections first
for collection in blender_obj.users_collection:
collection.objects.unlink(blender_obj)
# Delete the object directly
bpy.data.objects.remove(blender_obj)
# delete model card/currently loaded collections
for col in model_card.collections:
coll = bpy.data.collections.get(col.name)
if not coll:
continue
# unlink from scenes
for scene in bpy.data.scenes:
if scene.collection.children.get(coll.name):
scene.collection.children.unlink(coll)
bpy.data.collections.remove(coll)
def select_model_card_objects(model_card, context: Context):
# deselect all objects first
bpy.ops.object.select_all(action="DESELECT")
# select objects in model card
for obj in model_card.objects:
blender_obj = get_object_by_application_id(obj.applicationId)
if not blender_obj:
continue
if blender_obj.name in context.view_layer.objects:
blender_obj.select_set(True)
selected = context.selected_objects
if selected:
context.view_layer.objects.active = selected[0]
def zoom_to_selected_objects(context: Context):
"""
zooms to the selected objects
"""
bpy.ops.view3d.view_selected()
def model_card_exists(
project_id: str, model_id: str, is_publish: bool, context: Context
) -> bool:
"""
checks if a model card exists
"""
for model_card in context.scene.speckle_state.model_cards:
if (
model_card.project_id == project_id
and model_card.model_id == model_id
and model_card.is_publish == is_publish
):
return True
return False
+27 -48
View File
@@ -1,67 +1,46 @@
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_local_accounts, Account
from typing import List, Optional, Tuple
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.
from .account_manager import _client_cache
from .misc import format_relative_time, strip_non_ascii
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.
def get_models_for_project(
account_id: str, project_id: str, search: Optional[str] = None
) -> List[Tuple[str, str, str]]:
"""
fetches models for a given project from the Speckle server
"""
try:
# Validate inputs
if not account_id or not project_id:
print(f"Error: Invalid inputs - account_id: {account_id}, project_id: {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 []
client = _client_cache.get_client(account_id)
# 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 []
client.project.get(project_id)
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
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]
return [
(
strip_non_ascii(model.name),
model.id,
format_relative_time(model.updated_at),
)
for model in models
]
except Exception as e:
print(f"Error fetching models: {str(e)}")
# Clear cache on error to prevent stale clients
_client_cache.clear()
return []
+166 -46
View File
@@ -1,55 +1,175 @@
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
from typing import List, Optional, Tuple
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.
from specklepy.core.api.client import SpeckleClient
from specklepy.core.api.credentials import Account
from specklepy.core.api.inputs.project_inputs import WorksaceProjectsFilter
from specklepy.core.api.resources.current.workspace_resource import WorkspaceResource
from .account_manager import _client_cache
from .misc import format_relative_time, format_role, strip_non_ascii
def get_projects_for_account(
account_id: str, workspace_id: str, search: Optional[str] = None
) -> List[Tuple[str, str, str, str, bool]]:
"""
fetches projects for a given account from the Speckle server
"""
try:
# Get 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:
# Get cached client
client = _client_cache.get_client(account_id)
if not client:
print(f"Error: Could not get client for account: {account_id}")
return []
# 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]
# Get account for workspace operations that still need it
from specklepy.core.api.credentials import get_local_accounts
account: Optional[Account] = next(
(acc for acc in get_local_accounts() if acc.id == account_id), None
)
if not account:
print(f"Error: Could not find account with ID: {account_id}")
return []
if workspace_id == "personal":
return _get_personal_projects_with_permissions(client, search)
try:
workspace_resource = WorkspaceResource(
account, client.url, client.httpclient, client.server.version()
)
# create filter with search parameter
filter = (
WorksaceProjectsFilter(search=search, with_project_role_only=False)
if search
else None
)
projects_with_permissions = (
workspace_resource.get_projects_with_permissions(
workspace_id=workspace_id, limit=10, filter=filter
)
)
result = []
for project in projects_with_permissions.items:
can_load_permission = False
if hasattr(project, "permissions") and project.permissions:
can_load_permission = (
hasattr(project.permissions, "can_load")
and project.permissions.can_load
and project.permissions.can_load.authorized
)
result.append(
(
strip_non_ascii(project.name),
format_role(getattr(project, "role", ""))
if hasattr(project, "role") and project.role
else "",
format_relative_time(project.updated_at),
project.id,
can_load_permission,
)
)
return result
except Exception as workspace_error:
print(
f"WorkspaceResource failed, falling back to old method: {workspace_error}"
)
return _get_projects_with_individual_permissions(
client, workspace_id, search
)
except Exception as e:
import traceback
error_msg = f"Error: {str(e)}\n"
error_msg += f"Traceback:\n{''.join(traceback.format_tb(e.__traceback__))}"
print(error_msg)
print(error_msg)
# Clear cache on error to prevent stale clients
_client_cache.clear()
return []
def _get_personal_projects_with_permissions(
client: SpeckleClient, search: Optional[str] = None
) -> List[Tuple[str, str, str, str, bool]]:
"""
helper function to get personal projects with permissions using the old method
"""
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from .account_manager import can_load
filter = UserProjectsFilter(
search=search,
workspace_id=None,
personal_only=True,
include_implicit_access=True,
)
projects = client.active_user.get_projects(limit=10, filter=filter).items
result = []
for project in projects:
can_load_permission, _ = can_load(client, project)
result.append(
(
strip_non_ascii(project.name),
format_role(getattr(project, "role", ""))
if hasattr(project, "role") and project.role
else "",
format_relative_time(project.updated_at),
project.id,
can_load_permission,
)
)
return result
def _get_projects_with_individual_permissions(
client: SpeckleClient,
workspace_id: str,
search: Optional[str] = None,
) -> List[Tuple[str, str, str, str, bool]]:
"""
Fallback helper function to get projects with permissions using individual API calls
"""
from specklepy.core.api.inputs.user_inputs import UserProjectsFilter
from .account_manager import can_load
filter = UserProjectsFilter(
search=search,
workspace_id=workspace_id,
personal_only=False,
include_implicit_access=True,
)
projects = client.active_user.get_projects(limit=10, filter=filter).items
result = []
for project in projects:
can_load_permission, _ = can_load(client, project)
result.append(
(
strip_non_ascii(project.name),
format_role(getattr(project, "role", ""))
if hasattr(project, "role") and project.role
else "",
format_relative_time(project.updated_at),
project.id,
can_load_permission,
)
)
return result
@@ -0,0 +1,115 @@
import bpy
class speckle_project(bpy.types.PropertyGroup):
"""
PropertyGroup for storing project information
"""
name: bpy.props.StringProperty() # type: ignore
role: bpy.props.StringProperty(name="Role") # type: ignore
updated: bpy.props.StringProperty(name="Updated") # type: ignore
id: bpy.props.StringProperty(name="ID") # type: ignore
can_receive: bpy.props.BoolProperty(name="Can Receive", default=False) # type: ignore
class speckle_model(bpy.types.PropertyGroup):
"""
PropertyGroup for storing model information
"""
name: bpy.props.StringProperty() # type: ignore
id: bpy.props.StringProperty(name="ID") # type: ignore
updated: bpy.props.StringProperty(name="Updated") # type: ignore
class speckle_version(bpy.types.PropertyGroup):
"""
PropertyGroup for storing version information
"""
id: bpy.props.StringProperty(name="ID") # type: ignore
message: bpy.props.StringProperty(name="Message") # type: ignore
updated: bpy.props.StringProperty(name="Updated") # type: ignore
source_app: bpy.props.StringProperty(name="Source") # type: ignore
class speckle_object(bpy.types.PropertyGroup):
"""
PropertyGroup for storing object names and applicationIds
"""
name: bpy.props.StringProperty() # type: ignore
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
class speckle_collection(bpy.types.PropertyGroup):
"""
PropertyGroup for storing collections
"""
name: bpy.props.StringProperty() # type: ignore
applicationId: bpy.props.StringProperty(name="Application ID", default="") # type: ignore
class speckle_model_card(bpy.types.PropertyGroup):
"""
represents a Speckle model card in the Blender UI
"""
account_id: bpy.props.StringProperty(
name="Account ID", description="ID of the account", default=""
) # type: ignore
server_url: bpy.props.StringProperty(
name="Server URL",
description="URL of the Server",
default="app.speckle.systems",
) # type: ignore
project_name: bpy.props.StringProperty(
name="Project Name", description="Name of the project", default=""
) # type: ignore
project_id: bpy.props.StringProperty(
name="Project ID", description="ID of the selected project", default=""
) # type: ignore
model_id: bpy.props.StringProperty(
name="Model ID", description="ID of the model", default=""
) # type: ignore
model_name: bpy.props.StringProperty(
name="Model Name", description="Name of the model", default=""
) # type: ignore
is_publish: bpy.props.BoolProperty(
name="Publish/Load",
description="If the model is published or loaded",
default=False,
) # type: ignore
selection_summary: bpy.props.StringProperty(
name="Selection Summary", description="Summary of the selection", default=""
) # type: ignore
version_id: bpy.props.StringProperty(
name="Version ID", description="ID of the selected version", default=""
) # type: ignore
load_option: bpy.props.StringProperty(
name="Load Option", description="Option of loading the model", default=""
) # type: ignore
objects: bpy.props.CollectionProperty(type=speckle_object) # type: ignore
collections: bpy.props.CollectionProperty(type=speckle_collection) # type: ignore
instance_loading_mode: bpy.props.StringProperty(
name="Instance Loading Mode",
description="Mode of loading instances",
default="INSTANCE_PROXIES",
) # type: ignore
apply_modifiers: bpy.props.BoolProperty(
name="Apply Modifiers",
description="Apply modifiers to the objects",
default=True,
) # type: ignore
def get_model_card_id(self) -> str:
if not self.project_id or not self.model_id:
raise ValueError(
"Project ID and Model ID are required to generate a model card ID."
)
if self.is_publish:
return f"PUBLISH-{self.project_id}-{self.model_id}"
else:
return f"LOAD-{self.project_id}-{self.model_id}"
+52 -59
View File
@@ -1,92 +1,85 @@
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 typing import List, Tuple
from specklepy.core.api.client import SpeckleClient
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.
from .account_manager import _client_cache
from .misc import format_relative_time
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.
def get_versions_for_model(
account_id: str, project_id: str, model_id: str
) -> List[Tuple[str, str, str]]:
"""
fetches versions for a given model from the Speckle server
"""
try:
# Validate inputs
if not account_id or not project_id or not model_id:
print(f"Error: Invalid inputs - account_id: {account_id}, project_id: {project_id}, model_id: {model_id}")
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=[])
client: SpeckleClient = _client_cache.get_client(account_id)
# 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]
versions = client.version.get_versions(
project_id=project_id, model_id=model_id, limit=10
)
versions_list: List[Tuple[str, str, str]] = []
for version in versions.items:
if version.referenced_object != "":
versions_list.append(
(
version.id,
version.message
if version.message is not None
else "No message",
format_relative_time(version.created_at),
)
)
return versions_list
except Exception as e:
print(f"Error fetching versions: {str(e)}")
# Clear cache on error to prevent stale clients
_client_cache.clear()
return []
def get_latest_version(account_id: str, project_id: str, model_id: str) -> Tuple[str, str, str]:
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}")
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 cached client
client: SpeckleClient = _client_cache.get_client(account_id)
# Get versions (limit to 1 since we only need the latest)
versions: List[Version] = client.version.get_versions(project_id=project_id, model_id=model_id, limit=1).items
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))
return (
latest.id,
latest.message if latest.message is not None else "No message",
format_relative_time(latest.created_at),
)
except Exception as e:
print(f"Error fetching latest version: {str(e)}")
return ("", "", "")
# Clear cache on error to prevent stale clients
_client_cache.clear()
return ("", "", "")
+3 -2
View File
@@ -1,2 +1,3 @@
from ..converter.to_native import *
from ..converter.utils import *
from ..converter.to_native import * # noqa: F403
from ..converter.to_speckle import * # noqa: F403
from ..converter.utils import * # noqa: F403
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
from .to_speckle import convert_to_speckle # noqa: F401
from .material_to_speckle import ( # noqa: F401
blender_material_to_speckle,
create_render_material_proxies,
add_render_material_proxies_to_base,
)
@@ -0,0 +1,272 @@
from bpy.types import Object
from typing import Union, Optional, Tuple, List
from specklepy.objects.geometry import Polyline, Curve
from specklepy.objects.primitive import Interval
from specklepy.objects.base import Base
from mathutils import Matrix
from mathutils.geometry import interpolate_bezier
from .utils import nurb_make_curve, make_knots
def curve_to_speckle(
blender_obj: Object, scale_factor: float = 1.0
) -> Union[Base, None]:
assert blender_obj.type == "CURVE", "Object must be a curve"
assert blender_obj.data is not None, "Curve data cannot be None"
curve_data = blender_obj.data
matrix = blender_obj.matrix_world
units = "m" # TODO: Use the unit system from the scene
base = Base()
curves = []
for spline in curve_data.splines:
if spline.type == "BEZIER":
curves.append(
bezier_to_speckle(matrix, spline, blender_obj.name, scale_factor, units)
)
elif spline.type == "NURBS":
curves.append(
nurbs_to_speckle(matrix, spline, blender_obj.name, scale_factor, units)
)
if curves:
base["@elements"] = curves
base["name"] = blender_obj.name
return base
return None
def bezier_to_speckle(
matrix: Matrix,
spline,
name: Optional[str] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Curve:
degree = 3
closed = spline.use_cyclic_u
points: List[Tuple[float, float, float]] = []
for i, bp in enumerate(spline.bezier_points):
if i > 0:
transformed_point = matrix @ bp.handle_left * scale_factor
points.append(
(transformed_point.x, transformed_point.y, transformed_point.z)
)
transformed_point = matrix @ bp.co * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
if i < len(spline.bezier_points) - 1:
transformed_point = matrix @ bp.handle_right * scale_factor
points.append(
(transformed_point.x, transformed_point.y, transformed_point.z)
)
if closed:
transformed_point = (
matrix @ spline.bezier_points[-1].handle_right * scale_factor
)
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
transformed_point = matrix @ spline.bezier_points[0].handle_left * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
transformed_point = matrix @ spline.bezier_points[0].co * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
num_points = len(points)
flattened_points = []
for point in points:
flattened_points.extend(point)
knot_count = num_points + degree - 1
knots = [0] * knot_count
for i in range(1, len(knots)):
knots[i] = i // 3
length = spline.calc_length()
domain = Interval(start=0, end=length)
display_value = bezier_to_speckle_polyline(
matrix, spline, length, scale_factor, units
)
curve = Curve(
degree=degree,
periodic=not spline.use_endpoint_u,
rational=True,
points=flattened_points,
weights=[1] * num_points,
knots=knots,
closed=spline.use_cyclic_u,
displayValue=display_value,
units=units,
bbox=None,
)
curve.__dict__["_length"] = length
curve.__dict__["_area"] = 0.0
curve["domain"] = domain
if name:
curve["name"] = name
return curve
def bezier_to_speckle_polyline(
matrix: Matrix,
spline,
length: Optional[float] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Optional[Polyline]:
segments = len(spline.bezier_points)
if segments < 2:
return None
resolution = spline.resolution_u + 1
points: List[float] = []
if not spline.use_cyclic_u:
segments -= 1
for i in range(segments):
inext = (i + 1) % len(spline.bezier_points)
knot1 = spline.bezier_points[i].co
handle1 = spline.bezier_points[i].handle_right
handle2 = spline.bezier_points[inext].handle_left
knot2 = spline.bezier_points[inext].co
sampled_points = interpolate_bezier(knot1, handle1, handle2, knot2, resolution)
for p in sampled_points:
scaled_point = matrix @ p * scale_factor
points.append(scaled_point.x)
points.append(scaled_point.y)
points.append(scaled_point.z)
length = length or spline.calc_length()
polyline = Polyline(value=points, units=units)
polyline["domain"] = {"start": 0, "end": length}
polyline["closed"] = spline.use_cyclic_u
return polyline
def nurbs_to_speckle(
matrix: Matrix,
spline,
name: Optional[str] = None,
scale_factor: float = 1.0,
units: str = "m",
) -> Curve:
degree = spline.order_u - 1
knots = make_knots(spline)
length = spline.calc_length()
domain = Interval(start=0, end=length)
weights = [pt.weight for pt in spline.points]
first_weight = weights[0] if weights else 1.0
is_rational = any(abs(w - first_weight) > 1e-9 for w in weights)
points = []
for pt in spline.points:
transformed_point = matrix @ pt.co.xyz * scale_factor
points.append((transformed_point.x, transformed_point.y, transformed_point.z))
flattened_points = []
for point in points:
flattened_points.extend(point)
if spline.use_cyclic_u:
for i in range(0, degree * 3, 3):
flattened_points.append(flattened_points[i + 0])
flattened_points.append(flattened_points[i + 1])
flattened_points.append(flattened_points[i + 2])
for i in range(0, degree):
weights.append(weights[i])
resolution_multiplier = (
4 if (spline.use_cyclic_u and spline.point_count_u <= 16) else 1
)
display_value = nurbs_to_speckle_polyline(
matrix, spline, length, scale_factor, units, resolution_multiplier
)
curve = Curve(
degree=degree,
periodic=not spline.use_endpoint_u,
rational=is_rational,
points=flattened_points,
weights=weights,
knots=knots,
closed=spline.use_cyclic_u,
displayValue=display_value,
units=units,
bbox=None,
)
curve.__dict__["_length"] = length
curve["domain"] = domain
if name:
curve["name"] = name
return curve
def nurbs_to_speckle_polyline(
matrix: Matrix,
spline,
length: Optional[float] = None,
scale_factor: float = 1.0,
units: str = "m",
resolution_multiplier: int = 1,
) -> Polyline:
from mathutils import Vector
points: List[float] = []
resolution = spline.resolution_u * resolution_multiplier
sampled_points = nurb_make_curve(spline, resolution)
for i in range(0, len(sampled_points), 3):
point_vector = Vector(
(sampled_points[i], sampled_points[i + 1], sampled_points[i + 2])
)
transformed_point = matrix @ point_vector * scale_factor
points.append(transformed_point.x)
points.append(transformed_point.y)
points.append(transformed_point.z)
length = length or spline.calc_length()
polyline = Polyline(value=points, units=units)
polyline["domain"] = {"start": 0, "end": length}
polyline["closed"] = spline.use_cyclic_u
# Set length property if needed
if hasattr(polyline, "length") or hasattr(polyline, "_length"):
polyline.__dict__["_length"] = length
# Set area property if needed
if hasattr(polyline, "area") or hasattr(polyline, "_area"):
polyline.__dict__["_area"] = 0
return polyline
@@ -0,0 +1,257 @@
from typing import Dict, List, Set
import bpy
from bpy.types import Material, Object
from specklepy.objects.base import Base
from specklepy.objects.other import RenderMaterial
from specklepy.objects.proxies import RenderMaterialProxy
from ..utils import to_argb_int
from .utils import get_submesh_id, get_unique_id
def blender_material_to_speckle(material: Material) -> RenderMaterial:
"""
convert a Blender material to a Speckle RenderMaterial
"""
diffuse = -1 # default white
opacity = 1.0
emissive = -16777216 # default black
metalness = 0.0
roughness = 1.0
# extract material properties if using nodes
if material.use_nodes and material.node_tree:
output_node = None
for node in material.node_tree.nodes:
if node.type == "OUTPUT_MATERIAL":
output_node = node
break
# find the main shader node connected to output
main_shader = None
if output_node and output_node.inputs["Surface"].is_linked:
main_shader = output_node.inputs["Surface"].links[0].from_node
# handle different shader types
# we're supporting: principled, diffuse, emmision and glass - for now
if main_shader:
if main_shader.type == "BSDF_PRINCIPLED":
diffuse, opacity, metalness, roughness, emissive = (
_extract_principled_properties(main_shader)
)
elif main_shader.type == "BSDF_DIFFUSE":
color_input = main_shader.inputs.get("Color")
if color_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
diffuse = to_argb_int(rgba)
roughness = 1.0
elif main_shader.type == "EMISSION":
color_input = main_shader.inputs.get("Color")
strength_input = main_shader.inputs.get("Strength")
if color_input and strength_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
strength = (
float(strength_input.default_value)
if not strength_input.is_linked
else 1.0
)
if strength > 0:
emission_rgba = [c * strength for c in rgba[:3]] + [rgba[3]]
emission_rgba = [min(1.0, max(0.0, c)) for c in emission_rgba]
emissive = to_argb_int(emission_rgba)
diffuse = to_argb_int(rgba)
elif main_shader.type == "BSDF_GLASS":
color_input = main_shader.inputs.get("Color")
if color_input:
if color_input.is_linked:
rgba = _get_color_from_connected_node(
color_input.links[0].from_node
)
else:
rgba = list(color_input.default_value)
diffuse = to_argb_int(rgba)
roughness_input = main_shader.inputs.get("Roughness")
if roughness_input:
roughness = (
float(roughness_input.default_value)
if not roughness_input.is_linked
else 0.0
)
opacity = 0.5
else:
# fallback to legacy material properties
if hasattr(material, "diffuse_color"):
rgba = list(material.diffuse_color) + [1.0]
diffuse = to_argb_int(rgba)
if hasattr(material, "metallic"):
metalness = float(material.metallic)
if hasattr(material, "roughness"):
roughness = float(material.roughness)
render_material = RenderMaterial(
name=material.name,
diffuse=diffuse,
opacity=opacity,
emissive=emissive,
metalness=metalness,
roughness=roughness,
)
return render_material
def _extract_principled_properties(principled_node):
diffuse = -1
opacity = 1.0
metalness = 0.0
roughness = 1.0
emissive = -16777216
base_color_input = principled_node.inputs.get("Base Color")
if base_color_input:
if base_color_input.is_linked:
rgba = _get_color_from_connected_node(base_color_input.links[0].from_node)
else:
rgba = list(base_color_input.default_value)
diffuse = to_argb_int(rgba)
# Alpha/Opacity
alpha_input = principled_node.inputs.get("Alpha")
if alpha_input and not alpha_input.is_linked:
opacity = float(alpha_input.default_value)
# Metallic
metallic_input = principled_node.inputs.get("Metallic")
if metallic_input and not metallic_input.is_linked:
metalness = float(metallic_input.default_value)
# Roughness
roughness_input = principled_node.inputs.get("Roughness")
if roughness_input and not roughness_input.is_linked:
roughness = float(roughness_input.default_value)
# Emission - try different possible input names for different versions
emission_color_input = principled_node.inputs.get(
"Emission Color"
) or principled_node.inputs.get("Emission")
emission_strength_input = principled_node.inputs.get("Emission Strength")
if emission_color_input:
if emission_color_input.is_linked:
emission_rgba = _get_color_from_connected_node(
emission_color_input.links[0].from_node
)
else:
emission_rgba = list(emission_color_input.default_value)
emission_strength = 1.0
if emission_strength_input and not emission_strength_input.is_linked:
emission_strength = float(emission_strength_input.default_value)
if emission_strength > 0 and any(
c > 0.01 for c in emission_rgba[:3]
): # Check if color is not black
final_emission_rgba = [c * emission_strength for c in emission_rgba[:3]] + [
emission_rgba[3]
]
final_emission_rgba = [min(1.0, max(0.0, c)) for c in final_emission_rgba]
emissive = to_argb_int(final_emission_rgba)
return diffuse, opacity, metalness, roughness, emissive
def _get_color_from_connected_node(node):
if node.type == "RGB":
rgba = list(node.outputs["Color"].default_value)
return rgba
elif node.type == "VALTORGB":
if node.color_ramp.elements:
rgba = list(node.color_ramp.elements[0].color)
return rgba
elif hasattr(node, "color"):
rgba = list(node.color) + [1.0]
return rgba
# fallback to white
return [1.0, 1.0, 1.0, 1.0]
def collect_material_assignments(objects: List[Object]) -> Dict[str, Set[str]]:
"""
collect material assignments for objects, creating unique applicationIds
for each material slot use a unique id
"""
material_assignments: Dict[str, Set[str]] = {}
for obj in objects:
if not obj or not hasattr(obj, "data") or not obj.data:
continue
# check if object has materials
if hasattr(obj.data, "materials") and obj.data.materials:
for material_index, material_slot in enumerate(obj.data.materials):
if material_slot:
material_name = material_slot.name
# set unique ID for submeshes
application_id = get_submesh_id(obj, material_index)
if material_name not in material_assignments:
material_assignments[material_name] = set()
material_assignments[material_name].add(application_id)
return material_assignments
def create_render_material_proxies(objects: List[Object]) -> List[RenderMaterialProxy]:
material_assignments = collect_material_assignments(objects)
if not material_assignments:
return []
proxies = []
for material_name, object_ids in material_assignments.items():
blender_material = bpy.data.materials.get(material_name)
if not blender_material:
continue
speckle_material = blender_material_to_speckle(blender_material)
proxy = RenderMaterialProxy(objects=list(object_ids), value=speckle_material)
proxy.applicationId = get_unique_id(blender_material)
proxies.append(proxy)
return proxies
def add_render_material_proxies_to_base(base: Base, objects: List[Object]) -> None:
"""
add render material proxies to the base object.
"""
proxies = create_render_material_proxies(objects)
if proxies:
base.renderMaterialProxies = proxies
@@ -0,0 +1,132 @@
from typing import Dict, List, cast
import bpy
from bpy.types import Mesh as BMesh
from bpy.types import MeshPolygon, Object
from mathutils import Matrix as MMatrix
from mathutils import Vector as MVector
from specklepy.objects.base import Base
from specklepy.objects.geometry.mesh import Mesh
from .utils import get_submesh_id
def mesh_to_speckle(
blender_object: Object, data: bpy.types.Mesh, units_scale: float, units: str
) -> Base:
"""
convert a Blender mesh object
"""
meshes = mesh_to_speckle_meshes(blender_object, data, units_scale, units)
return meshes
def mesh_to_speckle_meshes(
blender_object: Object, data: bpy.types.Mesh, units_scale: float, units: str
) -> List[Mesh]:
"""
convert a Blender mesh to a list of Speckle meshes
each face corner (loop) gets its own vertex
"""
assert isinstance(data, BMesh), "Data must be a Blender mesh"
assert units_scale > 0, "Units scale must be positive"
submesh_data: Dict[int, List[MeshPolygon]] = {}
for p in data.polygons:
if p.material_index not in submesh_data:
submesh_data[p.material_index] = []
submesh_data[p.material_index].append(p)
transform = cast(MMatrix, blender_object.matrix_world)
normal_transform = transform.to_3x3().inverted().transposed()
submeshes = []
# sort material indices to ensure consistent ordering
for material_index in sorted(submesh_data.keys()):
mesh_area = 0
m_verts: List[float] = []
m_faces: List[int] = []
m_texcoords: List[float] = []
m_normals: List[float] = []
vertex_counter = 0
for face in submesh_data[material_index]:
mesh_area += face.area
loop_indices = face.loop_indices
m_faces.append(len(loop_indices))
for loop_index in loop_indices:
loop = data.loops[loop_index]
vertex = data.vertices[loop.vertex_index]
transformed_vertex = transform @ vertex.co * units_scale
m_verts.extend(
[transformed_vertex.x, transformed_vertex.y, transformed_vertex.z]
)
# get and transform the loop normal
# try to get split normal, fallback to face normal if not available
try:
if hasattr(loop, "normal") and len(loop.normal) > 0:
# Use split normal from loop
loop_normal = normal_transform @ loop.normal
else:
# Fallback to face normal
loop_normal = normal_transform @ face.normal
except: # noqa: E722
# Final fallback: use face normal
loop_normal = normal_transform @ face.normal
loop_normal.normalize()
m_normals.extend([loop_normal.x, loop_normal.y, loop_normal.z])
# add UV coordinates if available
if data.uv_layers.active:
uv_data = data.uv_layers.active.data[loop_index]
uv = cast(MVector, uv_data.uv)
m_texcoords.extend([uv.x, uv.y])
m_faces.append(vertex_counter)
vertex_counter += 1
speckle_mesh = Mesh(
vertices=m_verts,
faces=m_faces,
colors=[],
textureCoordinates=m_texcoords,
vertexNormals=m_normals,
units=units,
)
if len(m_verts) > 0:
speckle_mesh.area = mesh_area
speckle_mesh.applicationId = get_submesh_id(blender_object, material_index)
submeshes.append(speckle_mesh)
return submeshes
def is_closed_mesh(faces: List[int]) -> bool:
"""
check if a mesh is closed by verifying that each edge is shared by exactly 2 faces.
"""
edge_counts = {}
i = 0
while i < len(faces):
vertex_count = faces[i]
for j in range(vertex_count):
v1 = faces[i + 1 + j]
v2 = faces[i + 1 + ((j + 1) % vertex_count)]
edge = tuple(sorted([v1, v2]))
edge_counts[edge] = edge_counts.get(edge, 0) + 1
i += vertex_count + 1
return all(count == 2 for count in edge_counts.values())
@@ -0,0 +1,17 @@
from bpy.types import Object
from specklepy.objects.geometry import Point
def point_to_speckle(blender_object: Object, scale_factor: float = 1.0) -> Point:
assert blender_object.type == "EMPTY", "Object must be an empty."
location = blender_object.location
speckle_point = Point(
x=location.x * scale_factor,
y=location.y * scale_factor,
z=location.z * scale_factor,
units="", # TODO: implement units in object level
)
return speckle_point
@@ -0,0 +1,85 @@
from bpy.types import Object
from typing import Optional
from specklepy.objects.data_objects import BlenderObject
from .curve_to_speckle import curve_to_speckle
from .mesh_to_speckle import mesh_to_speckle_meshes
from .utils import get_object_id, get_curve_element_id
def convert_to_speckle(
blender_object: Object,
scale_factor: float = 1.0,
units: str = "m",
apply_modifiers: bool = True,
) -> Optional[BlenderObject]:
display_value = []
properties = {}
if blender_object.type == "CURVE":
# handle curve modifiers apply_modifiers is True
if apply_modifiers and blender_object.modifiers:
import bpy
# Convert curve with modifiers to mesh
depsgraph = bpy.context.evaluated_depsgraph_get()
evaluated_obj = blender_object.evaluated_get(depsgraph)
evaluated_mesh = evaluated_obj.to_mesh()
if evaluated_mesh:
meshes = mesh_to_speckle_meshes(
blender_object, evaluated_mesh, scale_factor, units
)
blender_object.to_mesh_clear()
if meshes:
display_value = meshes
else:
# curve conversion without modifiers
curve_result = curve_to_speckle(blender_object, scale_factor)
if curve_result and hasattr(curve_result, "@elements"):
display_value = curve_result["@elements"]
for i, element in enumerate(display_value):
if hasattr(element, "applicationId"):
element.applicationId = get_curve_element_id(blender_object, i)
elif curve_result:
if hasattr(curve_result, "applicationId"):
curve_result.applicationId = get_curve_element_id(blender_object, 0)
display_value = [curve_result]
elif blender_object.type == "MESH":
# get mesh data - apply modifiers if requested
mesh_data = blender_object.data
if apply_modifiers and blender_object.modifiers:
import bpy
# use evaluated object to get mesh with modifiers applied
depsgraph = bpy.context.evaluated_depsgraph_get()
evaluated_obj = blender_object.evaluated_get(depsgraph)
evaluated_mesh = evaluated_obj.to_mesh()
mesh_data = evaluated_mesh
meshes = mesh_to_speckle_meshes(blender_object, mesh_data, scale_factor, units)
if (
apply_modifiers
and blender_object.modifiers
and mesh_data != blender_object.data
):
blender_object.to_mesh_clear()
if meshes:
display_value = meshes
if not display_value:
return None
if not isinstance(display_value, list):
display_value = [display_value]
return BlenderObject(
name=blender_object.name,
type=blender_object.type,
displayValue=display_value,
applicationId=get_object_id(blender_object),
properties=properties,
units=units,
)
+242
View File
@@ -0,0 +1,242 @@
import bpy
from bpy.types import ID, Object
import math
from typing import Tuple, Optional
OBJECT_NAME_SPECKLE_SEPARATOR = " -- "
SPECKLE_ID_LENGTH = 32
_QUICK_TEST_NAME_LENGTH = SPECKLE_ID_LENGTH + len(OBJECT_NAME_SPECKLE_SEPARATOR)
def to_speckle_name(blender_object: bpy.types.ID) -> str:
does_name_contain_id = (
len(blender_object.name) > _QUICK_TEST_NAME_LENGTH
and OBJECT_NAME_SPECKLE_SEPARATOR in blender_object.name
)
if does_name_contain_id:
return blender_object.name.rsplit(OBJECT_NAME_SPECKLE_SEPARATOR, 1)[0]
else:
return blender_object.name
"""
Python implementation of Blender's NURBS curve generation for to Speckle conversion
from: https://blender.stackexchange.com/a/34276
based on https://projects.blender.org/blender/blender/src/branch/main/source/blender/blenkernel/intern/curve.cc (check old version)
"""
def macro_knotsu(nu: bpy.types.Spline) -> int:
return nu.order_u + nu.point_count_u + (nu.order_u - 1 if nu.use_cyclic_u else 0)
def macro_segmentsu(nu: bpy.types.Spline) -> int:
return nu.point_count_u if nu.use_cyclic_u else nu.point_count_u - 1
def make_knots(nu: bpy.types.Spline) -> list[float]:
knots = [0.0] * macro_knotsu(nu)
flag = nu.use_endpoint_u + (nu.use_bezier_u << 1)
if nu.use_cyclic_u:
calc_knots(knots, nu.point_count_u, nu.order_u, 0)
else:
calc_knots(knots, nu.point_count_u, nu.order_u, flag)
return knots
def calc_knots(knots: list[float], point_count: int, order: int, flag: int) -> None:
pts_order = point_count + order
if flag == 1: # CU_NURB_ENDPOINT
k = 0.0
for a in range(1, pts_order + 1):
knots[a - 1] = k
if a >= order and a <= point_count:
k += 1.0
elif flag == 2: # CU_NURB_BEZIER
if order == 4:
k = 0.34
for a in range(pts_order):
knots[a] = math.floor(k)
k += 1.0 / 3.0
elif order == 3:
k = 0.6
for a in range(pts_order):
if a >= order and a <= point_count:
k += 0.5
knots[a] = math.floor(k)
else:
for a in range(1, len(knots) - 1):
knots[a] = a - 1
knots[-1] = knots[-2]
def basis_nurb(
t: float,
order: int,
point_count: int,
knots: list[float],
basis: list[float],
start: int,
end: int,
) -> Tuple[int, int]:
i1 = i2 = 0
orderpluspnts = order + point_count
opp2 = orderpluspnts - 1
# this is for float inaccuracy
if t < knots[0]:
t = knots[0]
elif t > knots[opp2]:
t = knots[opp2]
# this part is order '1'
o2 = order + 1
for i in range(opp2):
if knots[i] != knots[i + 1] and t >= knots[i] and t <= knots[i + 1]:
basis[i] = 1.0
i1 = i - o2
if i1 < 0:
i1 = 0
i2 = i
i += 1
while i < opp2:
basis[i] = 0.0
i += 1
break
else:
basis[i] = 0.0
basis[i] = 0.0 # type: ignore
# this is order 2, 3, ...
for j in range(2, order + 1):
if i2 + j >= orderpluspnts:
i2 = opp2 - j
for i in range(i1, i2 + 1):
if basis[i] != 0.0:
d = ((t - knots[i]) * basis[i]) / (knots[i + j - 1] - knots[i])
else:
d = 0.0
if basis[i + 1] != 0.0:
e = ((knots[i + j] - t) * basis[i + 1]) / (knots[i + j] - knots[i + 1])
else:
e = 0.0
basis[i] = d + e
start = 1000
end = 0
for i in range(i1, i2 + 1):
if basis[i] > 0.0:
end = i
if start == 1000:
start = i
return start, end
def nurb_make_curve(nu: bpy.types.Spline, resolu: int, stride: int = 3) -> list[float]:
""" "BKE_nurb_makeCurve"""
EPS = 1e-6
coord_index = istart = iend = 0
coord_array = [0.0] * (3 * nu.resolution_u * macro_segmentsu(nu))
sum_array = [0] * nu.point_count_u
basisu = [0.0] * macro_knotsu(nu)
knots = make_knots(nu)
resolu = resolu * macro_segmentsu(nu)
ustart = knots[nu.order_u - 1]
uend = (
knots[nu.point_count_u + nu.order_u - 1]
if nu.use_cyclic_u
else knots[nu.point_count_u]
)
ustep = (uend - ustart) / (resolu - (0 if nu.use_cyclic_u else 1))
cycl = nu.order_u - 1 if nu.use_cyclic_u else 0
u = ustart
while resolu:
resolu -= 1
istart, iend = basis_nurb(
u, nu.order_u, nu.point_count_u + cycl, knots, basisu, istart, iend
)
# /* calc sum */
sumdiv = 0.0
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
sum_array[sum_index] = basisu[i] * nu.points[pt_index].co[3] # type: ignore
sumdiv += sum_array[sum_index]
sum_index += 1
if (sumdiv != 0.0) and (sumdiv < 1.0 - EPS or sumdiv > 1.0 + EPS):
sum_index = 0
for i in range(istart, iend + 1):
sum_array[sum_index] /= sumdiv # type: ignore
sum_index += 1
coord_array[coord_index : coord_index + 3] = (0.0, 0.0, 0.0)
sum_index = 0
pt_index = istart - 1
for i in range(istart, iend + 1):
if i >= nu.point_count_u:
pt_index = i - nu.point_count_u
else:
pt_index += 1
if sum_array[sum_index] != 0.0:
for j in range(3):
coord_array[coord_index + j] += (
sum_array[sum_index] * nu.points[pt_index].co[j]
)
sum_index += 1
coord_index += stride
u += ustep
return coord_array
def get_unique_id(native_object: ID, suffix: Optional[str] = None) -> str:
base_id = f"{type(native_object).__name__}:{native_object.name_full}"
if suffix:
return f"{base_id}:{suffix}"
return base_id
def get_submesh_id(blender_object: Object, material_index: int) -> str:
mesh_data = blender_object.data
if not mesh_data:
return f"Mesh:{blender_object.name_full}_mat{material_index}"
return f"Mesh:{mesh_data.name_full}_mat{material_index}"
def get_curve_element_id(blender_object: Object, curve_index: int = 0) -> str:
curve_data = blender_object.data
if not curve_data:
return f"Curve:{blender_object.name_full}_curve{curve_index}"
if curve_index == 0:
return f"Curve:{curve_data.name_full}"
return f"Curve:{curve_data.name_full}_curve{curve_index}"
def get_object_id(blender_object: Object) -> str:
return get_unique_id(blender_object)
+104 -6
View File
@@ -1,5 +1,10 @@
from typing import Tuple, List
from typing import Tuple, List, Optional
import bpy
import mathutils
from specklepy.objects import Base
from specklepy.objects.graph_traversal.default_traversal import (
create_default_traversal_function,
)
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
@@ -28,7 +33,6 @@ def create_material_from_proxy(
"""
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]
@@ -38,18 +42,14 @@ def create_material_from_proxy(
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 = (
@@ -87,4 +87,102 @@ def create_material_from_proxy(
)
bsdf.inputs["Emission Strength"].default_value = 1.0
# set viewport display color
if hasattr(render_material, "diffuse") and hasattr(render_material, "opacity"):
material.diffuse_color = (
diffuse_rgba[0],
diffuse_rgba[1],
diffuse_rgba[2],
opacity,
)
return material
def transform_matrix(transform: List[float]) -> mathutils.Matrix:
"""
converts a speckle transform array to a 4x4 matrix (blender needs it)
"""
if len(transform) != 16:
raise ValueError(f"Expected transform with 16 values, got {len(transform)}")
return mathutils.Matrix(
(
(transform[0], transform[4], transform[8], transform[12]),
(transform[1], transform[5], transform[9], transform[13]),
(transform[2], transform[6], transform[10], transform[14]),
(transform[3], transform[7], transform[11], transform[15]),
)
)
def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
"""
finds an object using traversal, checking both id and applicationId
"""
if hasattr(root_object, "__closure") and root_object.__closure:
if target_id in root_object.__closure:
if hasattr(root_object, "elements"):
for element in root_object.elements:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
return find_object_by_id(root_object, element.referencedId)
if hasattr(root_object, "@elements"):
for element in root_object["@elements"]:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
return find_object_by_id(root_object, element.referencedId)
traversal_function = create_default_traversal_function()
for traversal_item in traversal_function.traverse(root_object):
obj = traversal_item.current
if not hasattr(obj, "id"):
continue
if obj.id == target_id:
return obj
if hasattr(obj, "applicationId"):
app_id = obj.applicationId
if app_id == target_id:
return obj
def deep_search(search_obj):
if hasattr(search_obj, "id") and search_obj.id == target_id:
return search_obj
elements_attrs = ["elements", "@elements"]
for attr in elements_attrs:
if hasattr(search_obj, attr):
elements = getattr(search_obj, attr)
if elements and isinstance(elements, list):
for element in elements:
if hasattr(element, "id") and element.id == target_id:
return element
if (
hasattr(element, "referencedId")
and element.referencedId == target_id
):
ref_obj = find_object_by_id(
root_object, element.referencedId
)
if ref_obj:
return ref_obj
result = deep_search(element)
if result:
return result
return None
return deep_search(root_object)
+28 -47
View File
@@ -1,6 +1,7 @@
"""
Provides uniform and consistent path helpers for `specklepy`
"""
import os
import sys
from pathlib import 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,12 @@ 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"
print(completed_process.stdout)
print(completed_process.stderr)
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 +194,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 +203,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 +214,7 @@ def _import_dependencies() -> None:
# print(req)
# import_module("specklepy")
def ensure_dependencies(host_application: str) -> None:
try:
install_dependencies(host_application)
@@ -241,6 +222,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}!"
)
+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
uv pip compile pyproject.toml --output-file bpy_speckle/requirements.txt --generate-hashes
+9 -4
View File
@@ -1,6 +1,7 @@
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"
@@ -9,13 +10,16 @@ def patch_addon(simple_version: str):
with open(FILE_PATH, "r") as file:
lines = file.readlines()
for (index, line) in enumerate(lines):
for index, line in enumerate(lines):
if '"version":' in line:
lines[index] = f' "version": ({version[0]}, {version[1]}, {version[2]}),\n'
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):
"""Patches the connector version within the connector init file"""
FILE_PATH = "bpy_speckle/blender_manifest.toml"
@@ -24,8 +28,8 @@ def patch_manifest(simple_version: str):
with open(FILE_PATH, "r") as file:
lines = file.readlines()
for (index, line) in enumerate(lines):
if line.startswith('version ='):
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}")
break
@@ -33,6 +37,7 @@ def patch_manifest(simple_version: str):
with open(FILE_PATH, "w") as file:
file.writelines(lines)
def main():
tag = sys.argv[1]
if not re.match(r"([0-9]+)\.([0-9]+)\.([0-9]+)", tag):
+1 -1
View File
@@ -5,7 +5,7 @@ description = "Next-Gen Speckle connector for Blender!"
requires-python = ">=3.11.9, <4.0.0"
license = "Apache-2.0"
dependencies = [
"specklepy>=3.0.0a3"
"specklepy>=3.0.3",
]
[dependency-groups]
Generated
+415 -334
View File
@@ -13,15 +13,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.5.2"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/f9/9a7ce600ebe7804daf90d4d48b1c0510a4561ddce43a596be46676f82343/anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b", size = 171293 }
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/b4/f7e396030e3b11394436358ca258a81d6010106582422f23443c16ca1873/anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f", size = 89766 },
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
[[package]]
@@ -35,11 +36,11 @@ wheels = [
[[package]]
name = "attrs"
version = "25.2.0"
version = "25.3.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 }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/03/33/7a7388b9ef94aab40539939d94461ec682afbd895458945ed25be07f03f6/attrs-25.2.0-py3-none-any.whl", hash = "sha256:611344ff0a5fed735d86d7784610c84f8126b95e549bcad9ff61b4242f2d386b", size = 64016 },
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
]
[[package]]
@@ -53,77 +54,71 @@ wheels = [
[[package]]
name = "certifi"
version = "2024.8.30"
version = "2025.4.26"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.0"
version = "3.4.2"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/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 },
{ url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794 },
{ url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846 },
{ url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350 },
{ url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657 },
{ url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260 },
{ url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164 },
{ url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571 },
{ url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952 },
{ url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959 },
{ url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030 },
{ url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015 },
{ url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106 },
{ url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402 },
{ url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936 },
{ url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790 },
{ url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924 },
{ url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626 },
{ url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567 },
{ url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957 },
{ url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408 },
{ url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399 },
{ url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815 },
{ url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537 },
{ url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565 },
{ url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357 },
{ url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776 },
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
]
[[package]]
name = "deprecated"
version = "1.2.15"
version = "1.2.18"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941 },
{ url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 },
]
[[package]]
@@ -137,7 +132,7 @@ wheels = [
[[package]]
name = "gql"
version = "3.5.0"
version = "3.5.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -145,9 +140,9 @@ dependencies = [
{ 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 }
sdist = { url = "https://files.pythonhosted.org/packages/34/ed/44ffd30b06b3afc8274ee2f38c3c1b61fe4740bf03d92083e43d2c17ac77/gql-3.5.3.tar.gz", hash = "sha256:393b8c049d58e0d2f5461b9d738a2b5f904186a40395500b4a84dd092d56e42b", size = 180504 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/fb/01a200e1c31b79690427c8e983014e4220d2652b4372a46fe4598e1d7a8e/gql-3.5.0-py2.py3-none-any.whl", hash = "sha256:70dda5694a5b194a8441f077aa5fb70cc94e4ec08016117523f013680901ecb7", size = 74001 },
{ url = "https://files.pythonhosted.org/packages/cb/50/2f4e99b216821ac921dbebf91c644ba95818f5d07857acadee17220221f3/gql-3.5.3-py2.py3-none-any.whl", hash = "sha256:e1fcbde2893fcafdd28114ece87ff47f1cc339a31db271fc4e1d528f5a1d4fbc", size = 74348 },
]
[package.optional-dependencies]
@@ -161,33 +156,33 @@ websockets = [
[[package]]
name = "graphql-core"
version = "3.2.5"
version = "3.2.6"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/dc/078bd6b304de790618ebb95e2aedaadb78f4527ac43a9ad8815f006636b6/graphql_core-3.2.5-py3-none-any.whl", hash = "sha256:2f150d5096448aa4f8ab26268567bbfeef823769893b39c1a2e1409590939c8a", size = 203189 },
{ url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416 },
]
[[package]]
name = "h11"
version = "0.14.0"
version = "0.16.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 }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
version = "1.0.9"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[package]]
@@ -216,207 +211,260 @@ wheels = [
[[package]]
name = "multidict"
version = "6.1.0"
version = "6.4.4"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/91/2f/a3470242707058fe856fe59241eee5635d79087100b7042a867368863a27/multidict-6.4.4.tar.gz", hash = "sha256:69ee9e6ba214b5245031b76233dd95408a0fd57fdb019ddcc1ead4790932a8e8", size = 90183 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/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 },
{ url = "https://files.pythonhosted.org/packages/19/1b/4c6e638195851524a63972c5773c7737bea7e47b1ba402186a37773acee2/multidict-6.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4f5f29794ac0e73d2a06ac03fd18870adc0135a9d384f4a306a951188ed02f95", size = 65515 },
{ url = "https://files.pythonhosted.org/packages/25/d5/10e6bca9a44b8af3c7f920743e5fc0c2bcf8c11bf7a295d4cfe00b08fb46/multidict-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c04157266344158ebd57b7120d9b0b35812285d26d0e78193e17ef57bfe2979a", size = 38609 },
{ url = "https://files.pythonhosted.org/packages/26/b4/91fead447ccff56247edc7f0535fbf140733ae25187a33621771ee598a18/multidict-6.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb61ffd3ab8310d93427e460f565322c44ef12769f51f77277b4abad7b6f7223", size = 37871 },
{ url = "https://files.pythonhosted.org/packages/3b/37/cbc977cae59277e99d15bbda84cc53b5e0c4929ffd91d958347200a42ad0/multidict-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e0ba18a9afd495f17c351d08ebbc4284e9c9f7971d715f196b79636a4d0de44", size = 226661 },
{ url = "https://files.pythonhosted.org/packages/15/cd/7e0b57fbd4dc2fc105169c4ecce5be1a63970f23bb4ec8c721b67e11953d/multidict-6.4.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9faf1b1dcaadf9f900d23a0e6d6c8eadd6a95795a0e57fcca73acce0eb912065", size = 223422 },
{ url = "https://files.pythonhosted.org/packages/f1/01/1de268da121bac9f93242e30cd3286f6a819e5f0b8896511162d6ed4bf8d/multidict-6.4.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a4d1cb1327c6082c4fce4e2a438483390964c02213bc6b8d782cf782c9b1471f", size = 235447 },
{ url = "https://files.pythonhosted.org/packages/d2/8c/8b9a5e4aaaf4f2de14e86181a3a3d7b105077f668b6a06f043ec794f684c/multidict-6.4.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:941f1bec2f5dbd51feeb40aea654c2747f811ab01bdd3422a48a4e4576b7d76a", size = 231455 },
{ url = "https://files.pythonhosted.org/packages/35/db/e1817dcbaa10b319c412769cf999b1016890849245d38905b73e9c286862/multidict-6.4.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5f8a146184da7ea12910a4cec51ef85e44f6268467fb489c3caf0cd512f29c2", size = 223666 },
{ url = "https://files.pythonhosted.org/packages/4a/e1/66e8579290ade8a00e0126b3d9a93029033ffd84f0e697d457ed1814d0fc/multidict-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:232b7237e57ec3c09be97206bfb83a0aa1c5d7d377faa019c68a210fa35831f1", size = 217392 },
{ url = "https://files.pythonhosted.org/packages/7b/6f/f8639326069c24a48c7747c2a5485d37847e142a3f741ff3340c88060a9a/multidict-6.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:55ae0721c1513e5e3210bca4fc98456b980b0c2c016679d3d723119b6b202c42", size = 228969 },
{ url = "https://files.pythonhosted.org/packages/d2/c3/3d58182f76b960eeade51c89fcdce450f93379340457a328e132e2f8f9ed/multidict-6.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:51d662c072579f63137919d7bb8fc250655ce79f00c82ecf11cab678f335062e", size = 217433 },
{ url = "https://files.pythonhosted.org/packages/e1/4b/f31a562906f3bd375f3d0e83ce314e4a660c01b16c2923e8229b53fba5d7/multidict-6.4.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0e05c39962baa0bb19a6b210e9b1422c35c093b651d64246b6c2e1a7e242d9fd", size = 225418 },
{ url = "https://files.pythonhosted.org/packages/99/89/78bb95c89c496d64b5798434a3deee21996114d4d2c28dd65850bf3a691e/multidict-6.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b1cc3ab8c31d9ebf0faa6e3540fb91257590da330ffe6d2393d4208e638925", size = 235042 },
{ url = "https://files.pythonhosted.org/packages/74/91/8780a6e5885a8770442a8f80db86a0887c4becca0e5a2282ba2cae702bc4/multidict-6.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93ec84488a384cd7b8a29c2c7f467137d8a73f6fe38bb810ecf29d1ade011a7c", size = 230280 },
{ url = "https://files.pythonhosted.org/packages/68/c1/fcf69cabd542eb6f4b892469e033567ee6991d361d77abdc55e3a0f48349/multidict-6.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b308402608493638763abc95f9dc0030bbd6ac6aff784512e8ac3da73a88af08", size = 223322 },
{ url = "https://files.pythonhosted.org/packages/b8/85/5b80bf4b83d8141bd763e1d99142a9cdfd0db83f0739b4797172a4508014/multidict-6.4.4-cp311-cp311-win32.whl", hash = "sha256:343892a27d1a04d6ae455ecece12904d242d299ada01633d94c4f431d68a8c49", size = 35070 },
{ url = "https://files.pythonhosted.org/packages/09/66/0bed198ffd590ab86e001f7fa46b740d58cf8ff98c2f254e4a36bf8861ad/multidict-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:73484a94f55359780c0f458bbd3c39cb9cf9c182552177d2136e828269dee529", size = 38667 },
{ url = "https://files.pythonhosted.org/packages/d2/b5/5675377da23d60875fe7dae6be841787755878e315e2f517235f22f59e18/multidict-6.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:dc388f75a1c00000824bf28b7633e40854f4127ede80512b44c3cfeeea1839a2", size = 64293 },
{ url = "https://files.pythonhosted.org/packages/34/a7/be384a482754bb8c95d2bbe91717bf7ccce6dc38c18569997a11f95aa554/multidict-6.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:98af87593a666f739d9dba5d0ae86e01b0e1a9cfcd2e30d2d361fbbbd1a9162d", size = 38096 },
{ url = "https://files.pythonhosted.org/packages/66/6d/d59854bb4352306145bdfd1704d210731c1bb2c890bfee31fb7bbc1c4c7f/multidict-6.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aff4cafea2d120327d55eadd6b7f1136a8e5a0ecf6fb3b6863e8aca32cd8e50a", size = 37214 },
{ url = "https://files.pythonhosted.org/packages/99/e0/c29d9d462d7cfc5fc8f9bf24f9c6843b40e953c0b55e04eba2ad2cf54fba/multidict-6.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:169c4ba7858176b797fe551d6e99040c531c775d2d57b31bcf4de6d7a669847f", size = 224686 },
{ url = "https://files.pythonhosted.org/packages/dc/4a/da99398d7fd8210d9de068f9a1b5f96dfaf67d51e3f2521f17cba4ee1012/multidict-6.4.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b9eb4c59c54421a32b3273d4239865cb14ead53a606db066d7130ac80cc8ec93", size = 231061 },
{ url = "https://files.pythonhosted.org/packages/21/f5/ac11add39a0f447ac89353e6ca46666847051103649831c08a2800a14455/multidict-6.4.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7cf3bd54c56aa16fdb40028d545eaa8d051402b61533c21e84046e05513d5780", size = 232412 },
{ url = "https://files.pythonhosted.org/packages/d9/11/4b551e2110cded705a3c13a1d4b6a11f73891eb5a1c449f1b2b6259e58a6/multidict-6.4.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f682c42003c7264134bfe886376299db4cc0c6cd06a3295b41b347044bcb5482", size = 231563 },
{ url = "https://files.pythonhosted.org/packages/4c/02/751530c19e78fe73b24c3da66618eda0aa0d7f6e7aa512e46483de6be210/multidict-6.4.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920f9cf2abdf6e493c519492d892c362007f113c94da4c239ae88429835bad1", size = 223811 },
{ url = "https://files.pythonhosted.org/packages/c7/cb/2be8a214643056289e51ca356026c7b2ce7225373e7a1f8c8715efee8988/multidict-6.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:530d86827a2df6504526106b4c104ba19044594f8722d3e87714e847c74a0275", size = 216524 },
{ url = "https://files.pythonhosted.org/packages/19/f3/6d5011ec375c09081f5250af58de85f172bfcaafebff286d8089243c4bd4/multidict-6.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ecde56ea2439b96ed8a8d826b50c57364612ddac0438c39e473fafad7ae1c23b", size = 229012 },
{ url = "https://files.pythonhosted.org/packages/67/9c/ca510785df5cf0eaf5b2a8132d7d04c1ce058dcf2c16233e596ce37a7f8e/multidict-6.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:dc8c9736d8574b560634775ac0def6bdc1661fc63fa27ffdfc7264c565bcb4f2", size = 226765 },
{ url = "https://files.pythonhosted.org/packages/36/c8/ca86019994e92a0f11e642bda31265854e6ea7b235642f0477e8c2e25c1f/multidict-6.4.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7f3d3b3c34867579ea47cbd6c1f2ce23fbfd20a273b6f9e3177e256584f1eacc", size = 222888 },
{ url = "https://files.pythonhosted.org/packages/c6/67/bc25a8e8bd522935379066950ec4e2277f9b236162a73548a2576d4b9587/multidict-6.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:87a728af265e08f96b6318ebe3c0f68b9335131f461efab2fc64cc84a44aa6ed", size = 234041 },
{ url = "https://files.pythonhosted.org/packages/f1/a0/70c4c2d12857fccbe607b334b7ee28b6b5326c322ca8f73ee54e70d76484/multidict-6.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9f193eeda1857f8e8d3079a4abd258f42ef4a4bc87388452ed1e1c4d2b0c8740", size = 231046 },
{ url = "https://files.pythonhosted.org/packages/c1/0f/52954601d02d39742aab01d6b92f53c1dd38b2392248154c50797b4df7f1/multidict-6.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be06e73c06415199200e9a2324a11252a3d62030319919cde5e6950ffeccf72e", size = 227106 },
{ url = "https://files.pythonhosted.org/packages/af/24/679d83ec4379402d28721790dce818e5d6b9f94ce1323a556fb17fa9996c/multidict-6.4.4-cp312-cp312-win32.whl", hash = "sha256:622f26ea6a7e19b7c48dd9228071f571b2fbbd57a8cd71c061e848f281550e6b", size = 35351 },
{ url = "https://files.pythonhosted.org/packages/52/ef/40d98bc5f986f61565f9b345f102409534e29da86a6454eb6b7c00225a13/multidict-6.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:5e2bcda30d5009996ff439e02a9f2b5c3d64a20151d34898c000a6281faa3781", size = 38791 },
{ url = "https://files.pythonhosted.org/packages/df/2a/e166d2ffbf4b10131b2d5b0e458f7cee7d986661caceae0de8753042d4b2/multidict-6.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:82ffabefc8d84c2742ad19c37f02cde5ec2a1ee172d19944d380f920a340e4b9", size = 64123 },
{ url = "https://files.pythonhosted.org/packages/8c/96/e200e379ae5b6f95cbae472e0199ea98913f03d8c9a709f42612a432932c/multidict-6.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6a2f58a66fe2c22615ad26156354005391e26a2f3721c3621504cd87c1ea87bf", size = 38049 },
{ url = "https://files.pythonhosted.org/packages/75/fb/47afd17b83f6a8c7fa863c6d23ac5ba6a0e6145ed8a6bcc8da20b2b2c1d2/multidict-6.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5883d6ee0fd9d8a48e9174df47540b7545909841ac82354c7ae4cbe9952603bd", size = 37078 },
{ url = "https://files.pythonhosted.org/packages/fa/70/1af3143000eddfb19fd5ca5e78393985ed988ac493bb859800fe0914041f/multidict-6.4.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9abcf56a9511653fa1d052bfc55fbe53dbee8f34e68bd6a5a038731b0ca42d15", size = 224097 },
{ url = "https://files.pythonhosted.org/packages/b1/39/d570c62b53d4fba844e0378ffbcd02ac25ca423d3235047013ba2f6f60f8/multidict-6.4.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6ed5ae5605d4ad5a049fad2a28bb7193400700ce2f4ae484ab702d1e3749c3f9", size = 230768 },
{ url = "https://files.pythonhosted.org/packages/fd/f8/ed88f2c4d06f752b015933055eb291d9bc184936903752c66f68fb3c95a7/multidict-6.4.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbfcb60396f9bcfa63e017a180c3105b8c123a63e9d1428a36544e7d37ca9e20", size = 231331 },
{ url = "https://files.pythonhosted.org/packages/9c/6f/8e07cffa32f483ab887b0d56bbd8747ac2c1acd00dc0af6fcf265f4a121e/multidict-6.4.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0f1987787f5f1e2076b59692352ab29a955b09ccc433c1f6b8e8e18666f608b", size = 230169 },
{ url = "https://files.pythonhosted.org/packages/e6/2b/5dcf173be15e42f330110875a2668ddfc208afc4229097312212dc9c1236/multidict-6.4.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0121ccce8c812047d8d43d691a1ad7641f72c4f730474878a5aeae1b8ead8c", size = 222947 },
{ url = "https://files.pythonhosted.org/packages/39/75/4ddcbcebe5ebcd6faa770b629260d15840a5fc07ce8ad295a32e14993726/multidict-6.4.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83ec4967114295b8afd120a8eec579920c882831a3e4c3331d591a8e5bfbbc0f", size = 215761 },
{ url = "https://files.pythonhosted.org/packages/6a/c9/55e998ae45ff15c5608e384206aa71a11e1b7f48b64d166db400b14a3433/multidict-6.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:995f985e2e268deaf17867801b859a282e0448633f1310e3704b30616d269d69", size = 227605 },
{ url = "https://files.pythonhosted.org/packages/04/49/c2404eac74497503c77071bd2e6f88c7e94092b8a07601536b8dbe99be50/multidict-6.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d832c608f94b9f92a0ec8b7e949be7792a642b6e535fcf32f3e28fab69eeb046", size = 226144 },
{ url = "https://files.pythonhosted.org/packages/62/c5/0cd0c3c6f18864c40846aa2252cd69d308699cb163e1c0d989ca301684da/multidict-6.4.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d21c1212171cf7da703c5b0b7a0e85be23b720818aef502ad187d627316d5645", size = 221100 },
{ url = "https://files.pythonhosted.org/packages/71/7b/f2f3887bea71739a046d601ef10e689528d4f911d84da873b6be9194ffea/multidict-6.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cbebaa076aaecad3d4bb4c008ecc73b09274c952cf6a1b78ccfd689e51f5a5b0", size = 232731 },
{ url = "https://files.pythonhosted.org/packages/e5/b3/d9de808349df97fa75ec1372758701b5800ebad3c46ae377ad63058fbcc6/multidict-6.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c93a6fb06cc8e5d3628b2b5fda215a5db01e8f08fc15fadd65662d9b857acbe4", size = 229637 },
{ url = "https://files.pythonhosted.org/packages/5e/57/13207c16b615eb4f1745b44806a96026ef8e1b694008a58226c2d8f5f0a5/multidict-6.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8cd8f81f1310182362fb0c7898145ea9c9b08a71081c5963b40ee3e3cac589b1", size = 225594 },
{ url = "https://files.pythonhosted.org/packages/3a/e4/d23bec2f70221604f5565000632c305fc8f25ba953e8ce2d8a18842b9841/multidict-6.4.4-cp313-cp313-win32.whl", hash = "sha256:3e9f1cd61a0ab857154205fb0b1f3d3ace88d27ebd1409ab7af5096e409614cd", size = 35359 },
{ url = "https://files.pythonhosted.org/packages/a7/7a/cfe1a47632be861b627f46f642c1d031704cc1c0f5c0efbde2ad44aa34bd/multidict-6.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:8ffb40b74400e4455785c2fa37eba434269149ec525fc8329858c862e4b35373", size = 38903 },
{ url = "https://files.pythonhosted.org/packages/68/7b/15c259b0ab49938a0a1c8f3188572802704a779ddb294edc1b2a72252e7c/multidict-6.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6a602151dbf177be2450ef38966f4be3467d41a86c6a845070d12e17c858a156", size = 68895 },
{ url = "https://files.pythonhosted.org/packages/f1/7d/168b5b822bccd88142e0a3ce985858fea612404edd228698f5af691020c9/multidict-6.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d2b9712211b860d123815a80b859075d86a4d54787e247d7fbee9db6832cf1c", size = 40183 },
{ url = "https://files.pythonhosted.org/packages/e0/b7/d4b8d98eb850ef28a4922ba508c31d90715fd9b9da3801a30cea2967130b/multidict-6.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d2fa86af59f8fc1972e121ade052145f6da22758f6996a197d69bb52f8204e7e", size = 39592 },
{ url = "https://files.pythonhosted.org/packages/18/28/a554678898a19583548e742080cf55d169733baf57efc48c2f0273a08583/multidict-6.4.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50855d03e9e4d66eab6947ba688ffb714616f985838077bc4b490e769e48da51", size = 226071 },
{ url = "https://files.pythonhosted.org/packages/ee/dc/7ba6c789d05c310e294f85329efac1bf5b450338d2542498db1491a264df/multidict-6.4.4-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5bce06b83be23225be1905dcdb6b789064fae92499fbc458f59a8c0e68718601", size = 222597 },
{ url = "https://files.pythonhosted.org/packages/24/4f/34eadbbf401b03768dba439be0fb94b0d187facae9142821a3d5599ccb3b/multidict-6.4.4-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66ed0731f8e5dfd8369a883b6e564aca085fb9289aacabd9decd70568b9a30de", size = 228253 },
{ url = "https://files.pythonhosted.org/packages/c0/e6/493225a3cdb0d8d80d43a94503fc313536a07dae54a3f030d279e629a2bc/multidict-6.4.4-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:329ae97fc2f56f44d91bc47fe0972b1f52d21c4b7a2ac97040da02577e2daca2", size = 226146 },
{ url = "https://files.pythonhosted.org/packages/2f/70/e411a7254dc3bff6f7e6e004303b1b0591358e9f0b7c08639941e0de8bd6/multidict-6.4.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c27e5dcf520923d6474d98b96749e6805f7677e93aaaf62656005b8643f907ab", size = 220585 },
{ url = "https://files.pythonhosted.org/packages/08/8f/beb3ae7406a619100d2b1fb0022c3bb55a8225ab53c5663648ba50dfcd56/multidict-6.4.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:058cc59b9e9b143cc56715e59e22941a5d868c322242278d28123a5d09cdf6b0", size = 212080 },
{ url = "https://files.pythonhosted.org/packages/9c/ec/355124e9d3d01cf8edb072fd14947220f357e1c5bc79c88dff89297e9342/multidict-6.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:69133376bc9a03f8c47343d33f91f74a99c339e8b58cea90433d8e24bb298031", size = 226558 },
{ url = "https://files.pythonhosted.org/packages/fd/22/d2b95cbebbc2ada3be3812ea9287dcc9712d7f1a012fad041770afddb2ad/multidict-6.4.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d6b15c55721b1b115c5ba178c77104123745b1417527ad9641a4c5e2047450f0", size = 212168 },
{ url = "https://files.pythonhosted.org/packages/4d/c5/62bfc0b2f9ce88326dbe7179f9824a939c6c7775b23b95de777267b9725c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a887b77f51d3d41e6e1a63cf3bc7ddf24de5939d9ff69441387dfefa58ac2e26", size = 217970 },
{ url = "https://files.pythonhosted.org/packages/79/74/977cea1aadc43ff1c75d23bd5bc4768a8fac98c14e5878d6ee8d6bab743c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:632a3bf8f1787f7ef7d3c2f68a7bde5be2f702906f8b5842ad6da9d974d0aab3", size = 226980 },
{ url = "https://files.pythonhosted.org/packages/48/fc/cc4a1a2049df2eb84006607dc428ff237af38e0fcecfdb8a29ca47b1566c/multidict-6.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a145c550900deb7540973c5cdb183b0d24bed6b80bf7bddf33ed8f569082535e", size = 220641 },
{ url = "https://files.pythonhosted.org/packages/3b/6a/a7444d113ab918701988d4abdde373dbdfd2def7bd647207e2bf645c7eac/multidict-6.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc5d83c6619ca5c9672cb78b39ed8542f1975a803dee2cda114ff73cbb076edd", size = 221728 },
{ url = "https://files.pythonhosted.org/packages/2b/b0/fdf4c73ad1c55e0f4dbbf2aa59dd37037334091f9a4961646d2b7ac91a86/multidict-6.4.4-cp313-cp313t-win32.whl", hash = "sha256:3312f63261b9df49be9d57aaa6abf53a6ad96d93b24f9cc16cf979956355ce6e", size = 41913 },
{ url = "https://files.pythonhosted.org/packages/8e/92/27989ecca97e542c0d01d05a98a5ae12198a243a9ee12563a0313291511f/multidict-6.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:ba852168d814b2c73333073e1c7116d9395bea69575a01b0b3c89d2d5a87c8fb", size = 46112 },
{ url = "https://files.pythonhosted.org/packages/84/5d/e17845bb0fa76334477d5de38654d27946d5b5d3695443987a094a71b440/multidict-6.4.4-py3-none-any.whl", hash = "sha256:bd4557071b561a8b3b6075c3ce93cf9bfb6182cb241805c3d66ced3b75eff4ac", size = 10481 },
]
[[package]]
name = "propcache"
version = "0.2.0"
version = "0.3.2"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/a6/16/43264e4a779dd8588c21a70f0709665ee8f611211bdd2c87d952cfa7c776/propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168", size = 44139 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/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 },
{ url = "https://files.pythonhosted.org/packages/80/8d/e8b436717ab9c2cfc23b116d2c297305aa4cd8339172a456d61ebf5669b8/propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be", size = 74207 },
{ url = "https://files.pythonhosted.org/packages/d6/29/1e34000e9766d112171764b9fa3226fa0153ab565d0c242c70e9945318a7/propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f", size = 43648 },
{ url = "https://files.pythonhosted.org/packages/46/92/1ad5af0df781e76988897da39b5f086c2bf0f028b7f9bd1f409bb05b6874/propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9", size = 43496 },
{ url = "https://files.pythonhosted.org/packages/b3/ce/e96392460f9fb68461fabab3e095cb00c8ddf901205be4eae5ce246e5b7e/propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf", size = 217288 },
{ url = "https://files.pythonhosted.org/packages/c5/2a/866726ea345299f7ceefc861a5e782b045545ae6940851930a6adaf1fca6/propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9", size = 227456 },
{ url = "https://files.pythonhosted.org/packages/de/03/07d992ccb6d930398689187e1b3c718339a1c06b8b145a8d9650e4726166/propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66", size = 225429 },
{ url = "https://files.pythonhosted.org/packages/5d/e6/116ba39448753b1330f48ab8ba927dcd6cf0baea8a0ccbc512dfb49ba670/propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df", size = 213472 },
{ url = "https://files.pythonhosted.org/packages/a6/85/f01f5d97e54e428885a5497ccf7f54404cbb4f906688a1690cd51bf597dc/propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2", size = 204480 },
{ url = "https://files.pythonhosted.org/packages/e3/79/7bf5ab9033b8b8194cc3f7cf1aaa0e9c3256320726f64a3e1f113a812dce/propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7", size = 214530 },
{ url = "https://files.pythonhosted.org/packages/31/0b/bd3e0c00509b609317df4a18e6b05a450ef2d9a963e1d8bc9c9415d86f30/propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95", size = 205230 },
{ url = "https://files.pythonhosted.org/packages/7a/23/fae0ff9b54b0de4e819bbe559508da132d5683c32d84d0dc2ccce3563ed4/propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e", size = 206754 },
{ url = "https://files.pythonhosted.org/packages/b7/7f/ad6a3c22630aaa5f618b4dc3c3598974a72abb4c18e45a50b3cdd091eb2f/propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e", size = 218430 },
{ url = "https://files.pythonhosted.org/packages/5b/2c/ba4f1c0e8a4b4c75910742f0d333759d441f65a1c7f34683b4a74c0ee015/propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf", size = 223884 },
{ url = "https://files.pythonhosted.org/packages/88/e4/ebe30fc399e98572019eee82ad0caf512401661985cbd3da5e3140ffa1b0/propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e", size = 211480 },
{ url = "https://files.pythonhosted.org/packages/96/0a/7d5260b914e01d1d0906f7f38af101f8d8ed0dc47426219eeaf05e8ea7c2/propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897", size = 37757 },
{ url = "https://files.pythonhosted.org/packages/e1/2d/89fe4489a884bc0da0c3278c552bd4ffe06a1ace559db5ef02ef24ab446b/propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39", size = 41500 },
{ url = "https://files.pythonhosted.org/packages/a8/42/9ca01b0a6f48e81615dca4765a8f1dd2c057e0540f6116a27dc5ee01dfb6/propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10", size = 73674 },
{ url = "https://files.pythonhosted.org/packages/af/6e/21293133beb550f9c901bbece755d582bfaf2176bee4774000bd4dd41884/propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154", size = 43570 },
{ url = "https://files.pythonhosted.org/packages/0c/c8/0393a0a3a2b8760eb3bde3c147f62b20044f0ddac81e9d6ed7318ec0d852/propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615", size = 43094 },
{ url = "https://files.pythonhosted.org/packages/37/2c/489afe311a690399d04a3e03b069225670c1d489eb7b044a566511c1c498/propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db", size = 226958 },
{ url = "https://files.pythonhosted.org/packages/9d/ca/63b520d2f3d418c968bf596839ae26cf7f87bead026b6192d4da6a08c467/propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1", size = 234894 },
{ url = "https://files.pythonhosted.org/packages/11/60/1d0ed6fff455a028d678df30cc28dcee7af77fa2b0e6962ce1df95c9a2a9/propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c", size = 233672 },
{ url = "https://files.pythonhosted.org/packages/37/7c/54fd5301ef38505ab235d98827207176a5c9b2aa61939b10a460ca53e123/propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67", size = 224395 },
{ url = "https://files.pythonhosted.org/packages/ee/1a/89a40e0846f5de05fdc6779883bf46ba980e6df4d2ff8fb02643de126592/propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b", size = 212510 },
{ url = "https://files.pythonhosted.org/packages/5e/33/ca98368586c9566a6b8d5ef66e30484f8da84c0aac3f2d9aec6d31a11bd5/propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8", size = 222949 },
{ url = "https://files.pythonhosted.org/packages/ba/11/ace870d0aafe443b33b2f0b7efdb872b7c3abd505bfb4890716ad7865e9d/propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251", size = 217258 },
{ url = "https://files.pythonhosted.org/packages/5b/d2/86fd6f7adffcfc74b42c10a6b7db721d1d9ca1055c45d39a1a8f2a740a21/propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474", size = 213036 },
{ url = "https://files.pythonhosted.org/packages/07/94/2d7d1e328f45ff34a0a284cf5a2847013701e24c2a53117e7c280a4316b3/propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535", size = 227684 },
{ url = "https://files.pythonhosted.org/packages/b7/05/37ae63a0087677e90b1d14710e532ff104d44bc1efa3b3970fff99b891dc/propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06", size = 234562 },
{ url = "https://files.pythonhosted.org/packages/a4/7c/3f539fcae630408d0bd8bf3208b9a647ccad10976eda62402a80adf8fc34/propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1", size = 222142 },
{ url = "https://files.pythonhosted.org/packages/7c/d2/34b9eac8c35f79f8a962546b3e97e9d4b990c420ee66ac8255d5d9611648/propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1", size = 37711 },
{ url = "https://files.pythonhosted.org/packages/19/61/d582be5d226cf79071681d1b46b848d6cb03d7b70af7063e33a2787eaa03/propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c", size = 41479 },
{ url = "https://files.pythonhosted.org/packages/dc/d1/8c747fafa558c603c4ca19d8e20b288aa0c7cda74e9402f50f31eb65267e/propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945", size = 71286 },
{ url = "https://files.pythonhosted.org/packages/61/99/d606cb7986b60d89c36de8a85d58764323b3a5ff07770a99d8e993b3fa73/propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252", size = 42425 },
{ url = "https://files.pythonhosted.org/packages/8c/96/ef98f91bbb42b79e9bb82bdd348b255eb9d65f14dbbe3b1594644c4073f7/propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f", size = 41846 },
{ url = "https://files.pythonhosted.org/packages/5b/ad/3f0f9a705fb630d175146cd7b1d2bf5555c9beaed54e94132b21aac098a6/propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33", size = 208871 },
{ url = "https://files.pythonhosted.org/packages/3a/38/2085cda93d2c8b6ec3e92af2c89489a36a5886b712a34ab25de9fbca7992/propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e", size = 215720 },
{ url = "https://files.pythonhosted.org/packages/61/c1/d72ea2dc83ac7f2c8e182786ab0fc2c7bd123a1ff9b7975bee671866fe5f/propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1", size = 215203 },
{ url = "https://files.pythonhosted.org/packages/af/81/b324c44ae60c56ef12007105f1460d5c304b0626ab0cc6b07c8f2a9aa0b8/propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3", size = 206365 },
{ url = "https://files.pythonhosted.org/packages/09/73/88549128bb89e66d2aff242488f62869014ae092db63ccea53c1cc75a81d/propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1", size = 196016 },
{ url = "https://files.pythonhosted.org/packages/b9/3f/3bdd14e737d145114a5eb83cb172903afba7242f67c5877f9909a20d948d/propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6", size = 205596 },
{ url = "https://files.pythonhosted.org/packages/0f/ca/2f4aa819c357d3107c3763d7ef42c03980f9ed5c48c82e01e25945d437c1/propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387", size = 200977 },
{ url = "https://files.pythonhosted.org/packages/cd/4a/e65276c7477533c59085251ae88505caf6831c0e85ff8b2e31ebcbb949b1/propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4", size = 197220 },
{ url = "https://files.pythonhosted.org/packages/7c/54/fc7152e517cf5578278b242396ce4d4b36795423988ef39bb8cd5bf274c8/propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88", size = 210642 },
{ url = "https://files.pythonhosted.org/packages/b9/80/abeb4a896d2767bf5f1ea7b92eb7be6a5330645bd7fb844049c0e4045d9d/propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206", size = 212789 },
{ url = "https://files.pythonhosted.org/packages/b3/db/ea12a49aa7b2b6d68a5da8293dcf50068d48d088100ac016ad92a6a780e6/propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43", size = 205880 },
{ url = "https://files.pythonhosted.org/packages/d1/e5/9076a0bbbfb65d1198007059c65639dfd56266cf8e477a9707e4b1999ff4/propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02", size = 37220 },
{ url = "https://files.pythonhosted.org/packages/d3/f5/b369e026b09a26cd77aa88d8fffd69141d2ae00a2abaaf5380d2603f4b7f/propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05", size = 40678 },
{ url = "https://files.pythonhosted.org/packages/a4/3a/6ece377b55544941a08d03581c7bc400a3c8cd3c2865900a68d5de79e21f/propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b", size = 76560 },
{ url = "https://files.pythonhosted.org/packages/0c/da/64a2bb16418740fa634b0e9c3d29edff1db07f56d3546ca2d86ddf0305e1/propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0", size = 44676 },
{ url = "https://files.pythonhosted.org/packages/36/7b/f025e06ea51cb72c52fb87e9b395cced02786610b60a3ed51da8af017170/propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e", size = 44701 },
{ url = "https://files.pythonhosted.org/packages/a4/00/faa1b1b7c3b74fc277f8642f32a4c72ba1d7b2de36d7cdfb676db7f4303e/propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28", size = 276934 },
{ url = "https://files.pythonhosted.org/packages/74/ab/935beb6f1756e0476a4d5938ff44bf0d13a055fed880caf93859b4f1baf4/propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a", size = 278316 },
{ url = "https://files.pythonhosted.org/packages/f8/9d/994a5c1ce4389610838d1caec74bdf0e98b306c70314d46dbe4fcf21a3e2/propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c", size = 282619 },
{ url = "https://files.pythonhosted.org/packages/2b/00/a10afce3d1ed0287cef2e09506d3be9822513f2c1e96457ee369adb9a6cd/propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725", size = 265896 },
{ url = "https://files.pythonhosted.org/packages/2e/a8/2aa6716ffa566ca57c749edb909ad27884680887d68517e4be41b02299f3/propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892", size = 252111 },
{ url = "https://files.pythonhosted.org/packages/36/4f/345ca9183b85ac29c8694b0941f7484bf419c7f0fea2d1e386b4f7893eed/propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44", size = 268334 },
{ url = "https://files.pythonhosted.org/packages/3e/ca/fcd54f78b59e3f97b3b9715501e3147f5340167733d27db423aa321e7148/propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe", size = 255026 },
{ url = "https://files.pythonhosted.org/packages/8b/95/8e6a6bbbd78ac89c30c225210a5c687790e532ba4088afb8c0445b77ef37/propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81", size = 250724 },
{ url = "https://files.pythonhosted.org/packages/ee/b0/0dd03616142baba28e8b2d14ce5df6631b4673850a3d4f9c0f9dd714a404/propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba", size = 268868 },
{ url = "https://files.pythonhosted.org/packages/c5/98/2c12407a7e4fbacd94ddd32f3b1e3d5231e77c30ef7162b12a60e2dd5ce3/propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770", size = 271322 },
{ url = "https://files.pythonhosted.org/packages/35/91/9cb56efbb428b006bb85db28591e40b7736847b8331d43fe335acf95f6c8/propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330", size = 265778 },
{ url = "https://files.pythonhosted.org/packages/9a/4c/b0fe775a2bdd01e176b14b574be679d84fc83958335790f7c9a686c1f468/propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394", size = 41175 },
{ url = "https://files.pythonhosted.org/packages/a4/ff/47f08595e3d9b5e149c150f88d9714574f1a7cbd89fe2817158a952674bf/propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198", size = 44857 },
{ url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
version = "2.11.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
version = "2.33.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 }
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/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 },
{ url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584 },
{ url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071 },
{ url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823 },
{ url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792 },
{ url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338 },
{ url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998 },
{ url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200 },
{ url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890 },
{ url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359 },
{ url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883 },
{ url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074 },
{ url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538 },
{ url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909 },
{ url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786 },
{ url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 },
{ url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 },
{ url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 },
{ url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 },
{ url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 },
{ url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 },
{ url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 },
{ url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 },
{ url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 },
{ url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 },
{ url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 },
{ url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 },
{ url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 },
{ url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 },
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 },
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 },
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 },
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 },
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 },
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 },
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 },
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 },
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 },
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 },
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 },
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 },
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 },
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 },
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
{ url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200 },
{ url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123 },
{ url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852 },
{ url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484 },
{ url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896 },
{ url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475 },
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 },
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 },
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 },
]
[[package]]
name = "pydantic-settings"
version = "2.8.1"
version = "2.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
version = "1.1.0"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
]
[[package]]
name = "requests"
version = "2.32.3"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
@@ -424,9 +472,9 @@ dependencies = [
{ 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 }
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 },
]
[[package]]
@@ -489,7 +537,7 @@ dev = [
]
[package.metadata]
requires-dist = [{ name = "specklepy", specifier = ">=3.0.0a3" }]
requires-dist = [{ name = "specklepy", specifier = ">=3.0.1" }]
[package.metadata.requires-dev]
dev = [
@@ -499,7 +547,7 @@ dev = [
[[package]]
name = "specklepy"
version = "3.0.0a3"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "appdirs" },
@@ -509,27 +557,32 @@ dependencies = [
{ 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 }
sdist = { url = "https://files.pythonhosted.org/packages/f4/e1/92a88f3651271177200e1c222720c125d317eb95766dd3a68548f50d4ba5/specklepy-3.0.1.tar.gz", hash = "sha256:a535e73cb378cf7dee35c60a45b48b2c4866d5216b9abcf0426d8223091d3a2e", size = 201758 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/30/89e57b11b6908057c95e015fe66f024d8c60fbb50368d9fb8608e316cdf8/specklepy-3.0.0a3-py3-none-any.whl", hash = "sha256:6d342c138807b310d3b27995cde6ac955cba4c2e6735b01dee3376f9e1db874d", size = 110320 },
{ url = "https://files.pythonhosted.org/packages/93/c4/fa934c65d6cb9351da126a2a2e738c57362da415b7acc10fe43f2aa17efa/specklepy-3.0.1-py3-none-any.whl", hash = "sha256:91cd6a32c462db9969cc4872f67b97ae0c861bdb75b78aba9597ef67936aa675", size = 110323 },
]
[[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"
version = "4.14.0"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
{ url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839 },
]
[[package]]
name = "typing-inspection"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
]
[[package]]
@@ -572,11 +625,11 @@ wheels = [
[[package]]
name = "urllib3"
version = "2.2.3"
version = "2.4.0"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 },
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
]
[[package]]
@@ -601,107 +654,135 @@ wheels = [
[[package]]
name = "wrapt"
version = "1.17.0"
version = "1.17.2"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/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 },
{ url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 },
{ url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 },
{ url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 },
{ url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 },
{ url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 },
{ url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 },
{ url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 },
{ url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 },
{ url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 },
{ url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 },
{ url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 },
{ url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 },
{ url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 },
{ url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 },
{ url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 },
{ url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 },
{ url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 },
{ url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 },
{ url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 },
{ url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 },
{ url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 },
{ url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 },
{ url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 },
{ url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 },
{ url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 },
{ url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 },
{ url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 },
{ url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 },
{ url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 },
{ url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 },
{ url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 },
{ url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 },
{ url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 },
{ url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 },
{ url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 },
{ url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 },
{ url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 },
{ url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 },
{ url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 },
{ url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 },
{ url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 },
{ url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 },
{ url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 },
{ url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 },
{ url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 },
]
[[package]]
name = "yarl"
version = "1.15.2"
version = "1.20.1"
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 }
sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/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 },
{ url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833 },
{ url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070 },
{ url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818 },
{ url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003 },
{ url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537 },
{ url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358 },
{ url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362 },
{ url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979 },
{ url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274 },
{ url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294 },
{ url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169 },
{ url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776 },
{ url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341 },
{ url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988 },
{ url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113 },
{ url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485 },
{ url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686 },
{ url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667 },
{ url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025 },
{ url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709 },
{ url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287 },
{ url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429 },
{ url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429 },
{ url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862 },
{ url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616 },
{ url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954 },
{ url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575 },
{ url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061 },
{ url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142 },
{ url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894 },
{ url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378 },
{ url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069 },
{ url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249 },
{ url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710 },
{ url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811 },
{ url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078 },
{ url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748 },
{ url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595 },
{ url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616 },
{ url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324 },
{ url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676 },
{ url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614 },
{ url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766 },
{ url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615 },
{ url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982 },
{ url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792 },
{ url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049 },
{ url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774 },
{ url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252 },
{ url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198 },
{ url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346 },
{ url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826 },
{ url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217 },
{ url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700 },
{ url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644 },
{ url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452 },
{ url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378 },
{ url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261 },
{ url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987 },
{ url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361 },
{ url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460 },
{ url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486 },
{ url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219 },
{ url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693 },
{ url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803 },
{ url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709 },
{ url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591 },
{ url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003 },
{ url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542 },
]