Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a1f835dc77 | |||
| e2172216a5 | |||
| 965c3e9c6e | |||
| 65e4812ba1 | |||
| 87df86f723 | |||
| fd32371be3 | |||
| 19c1334bb3 | |||
| 7a36450143 | |||
| d37fce644b | |||
| 00bcefba56 | |||
| d53bc064e1 | |||
| 50affbedf1 | |||
| f00aeecead | |||
| bbf2ee79be | |||
| abb1f042d4 | |||
| 04b4e02d05 | |||
| 144990db7a | |||
| a5592cdd7d | |||
| 249e3a0a84 | |||
| f869139d2a | |||
| 3954369db5 | |||
| 7165d99b76 | |||
| 8234db872d | |||
| 9acc84387f | |||
| 4eb1f2b773 | |||
| 4d60607a92 | |||
| 09f809fc8f | |||
| 9f6833c36e | |||
| 1cc8ed804a | |||
| eb0f036641 | |||
| f140c41af6 | |||
| 8a91634f55 | |||
| b025819307 | |||
| 3ec98d3dd2 | |||
| ba182d48c4 | |||
| f35853cff1 | |||
| d64ad50d32 | |||
| ab0b012f58 | |||
| 8c39da370f | |||
| 532eba37a5 | |||
| 39207e5716 | |||
| 75763d0929 | |||
| 2f2a67a569 | |||
| 7246d239be | |||
| 4d1fe83c1e | |||
| 1097bba539 | |||
| eb25b6d821 | |||
| 80515fdc69 |
+17
-2
@@ -38,7 +38,7 @@ 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_workspace
|
||||
from .connector.ui.model_selection_dialog import SPECKLE_OT_model_selection_dialog, speckle_model, SPECKLE_UL_models_list
|
||||
from .connector.ui.version_selection_dialog import SPECKLE_OT_version_selection_dialog, speckle_version, SPECKLE_UL_versions_list
|
||||
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog
|
||||
from .connector.ui.selection_filter_dialog import SPECKLE_OT_selection_filter_dialog, speckle_object
|
||||
from .connector.ui.model_card import speckle_model_card
|
||||
# Operators
|
||||
from .connector.blender_operators.publish_button import SPECKLE_OT_publish
|
||||
@@ -81,6 +81,21 @@ def invoke_window_manager_properties():
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
def save_model_cards(scene):
|
||||
model_cards_data = [card.to_dict() for card in scene.speckle_state.model_cards]
|
||||
@@ -103,7 +118,7 @@ classes = (
|
||||
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_OT_selection_filter_dialog, speckle_object,
|
||||
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,
|
||||
|
||||
@@ -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://app.speckle.systems/connectors"
|
||||
|
||||
# Optional list defined by Blender and server, see:
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
||||
|
||||
@@ -3,10 +3,12 @@ from bpy.types import Context
|
||||
from bpy.types import Event
|
||||
from typing import Set
|
||||
|
||||
from ..operations.publish_operation import publish_operation
|
||||
from ..utils.account_manager import get_server_url_by_account_id
|
||||
|
||||
|
||||
class SPECKLE_OT_publish(bpy.types.Operator):
|
||||
bl_idname = "speckle.publish"
|
||||
|
||||
bl_label = "Publish to Speckle"
|
||||
bl_description = "Publish selected objects to Speckle"
|
||||
|
||||
@@ -14,7 +16,78 @@ class SPECKLE_OT_publish(bpy.types.Operator):
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
context.scene.speckle_state.ui_mode = "PUBLISH"
|
||||
wm = context.window_manager
|
||||
|
||||
bpy.ops.speckle.project_selection_dialog("INVOKE_DEFAULT")
|
||||
# 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)
|
||||
|
||||
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"
|
||||
):
|
||||
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.collection_name = (
|
||||
f"{getattr(wm, 'selected_model_name', 'Model')} - {version_id[:8]}"
|
||||
)
|
||||
|
||||
# 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 = ""
|
||||
|
||||
self.report({"INFO"}, message)
|
||||
return {"FINISHED"}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
from ..operations.load_operation import load_operation # noqa: F401
|
||||
from ..operations.publish_operation import publish_operation # noqa: F401
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import bpy
|
||||
from bpy.types import Context
|
||||
from specklepy.api.credentials import get_local_accounts
|
||||
from specklepy.core.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.core.api import operations
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.objects.models.collections.collection import Collection as SCollection
|
||||
from specklepy.objects.graph_traversal.default_traversal import (
|
||||
create_default_traversal_function,
|
||||
)
|
||||
from specklepy.core.api import host_applications
|
||||
|
||||
from ..utils.get_ascendants import get_ascendants
|
||||
from ...converter.utils import find_object_by_id
|
||||
from ...converter.utils import find_object_by_id, get_project_workspace_id
|
||||
from ...converter.to_native import (
|
||||
convert_to_native,
|
||||
render_material_proxy_to_native,
|
||||
instance_definition_proxy_to_native,
|
||||
find_instance_definitions,
|
||||
)
|
||||
from specklepy.logging import metrics
|
||||
from ... import bl_info
|
||||
|
||||
|
||||
def load_operation(context: Context) -> None:
|
||||
@@ -52,6 +55,23 @@ def load_operation(context: Context) -> None:
|
||||
|
||||
version_data = operations.receive(obj_id, transport)
|
||||
|
||||
metrics.set_host_app("blender")
|
||||
|
||||
metrics.track(
|
||||
metrics.RECEIVE,
|
||||
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 != account.userInfo.id,
|
||||
"workspace_id": get_project_workspace_id(client, wm.selected_project_id),
|
||||
},
|
||||
)
|
||||
|
||||
# Create material mapping first
|
||||
material_mapping = render_material_proxy_to_native(version_data)
|
||||
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
import bpy
|
||||
from bpy.types import Context, Collection as BlenderCollection
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.models.collections.collection import Collection
|
||||
from specklepy.core.api import operations
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.transports.server import ServerTransport
|
||||
from specklepy.core.api.inputs.version_inputs import CreateVersionInput
|
||||
from specklepy.core.api.credentials import get_local_accounts
|
||||
from specklepy.objects.models.units import Units
|
||||
|
||||
from ...converter.to_speckle import convert_to_speckle
|
||||
from ...converter.to_speckle.material_to_speckle import (
|
||||
add_render_material_proxies_to_base,
|
||||
)
|
||||
from ...converter.utils import get_project_workspace_id
|
||||
from specklepy.logging import metrics
|
||||
from ... import bl_info
|
||||
|
||||
|
||||
def publish_operation(
|
||||
context: Context, objects_to_convert: List
|
||||
) -> Tuple[bool, str, Optional[str]]:
|
||||
"""
|
||||
publish objects to speckle
|
||||
"""
|
||||
wm = context.window_manager
|
||||
|
||||
try:
|
||||
# get account and authenticate
|
||||
account = next(
|
||||
(acc for acc in get_local_accounts() if acc.id == wm.selected_account_id),
|
||||
None,
|
||||
)
|
||||
|
||||
if account is None:
|
||||
return False, "No Speckle account found", None
|
||||
|
||||
client = SpeckleClient(host=account.serverInfo.url)
|
||||
client.authenticate_with_account(account)
|
||||
|
||||
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)
|
||||
|
||||
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(
|
||||
objectId=obj_id,
|
||||
modelId=wm.selected_model_id,
|
||||
projectId=wm.selected_project_id,
|
||||
message="",
|
||||
sourceApplication="blender",
|
||||
)
|
||||
|
||||
version = client.version.create(version_input)
|
||||
version_id = version.id
|
||||
|
||||
# track metrics
|
||||
metrics.set_host_app("blender")
|
||||
metrics.track(
|
||||
metrics.SEND,
|
||||
account,
|
||||
{
|
||||
"ui": "dui3",
|
||||
"hostAppVersion": ".".join(map(str, bl_info["blender"])),
|
||||
"core_version": ".".join(map(str, bl_info["version"])),
|
||||
"workspace_id": get_project_workspace_id(
|
||||
client, wm.selected_project_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()
|
||||
return False, f"Failed to publish: {str(e)}", None
|
||||
|
||||
|
||||
def build_collection_hierarchy(
|
||||
context: Context, objects_to_convert: List
|
||||
) -> 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)
|
||||
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
|
||||
) -> 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)
|
||||
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
|
||||
@@ -11,7 +11,6 @@ class SpeckleState(PropertyGroup):
|
||||
manages the state of the Speckle addon in Blender
|
||||
"""
|
||||
|
||||
ui_mode: StringProperty(name="UI Mode", default="NONE") # type: ignore
|
||||
model_cards: CollectionProperty(type=speckle_model_card) # type: ignore
|
||||
|
||||
def get_model_card_by_id(self, model_card_id: str) -> Optional[speckle_model_card]:
|
||||
|
||||
@@ -17,7 +17,7 @@ class SPECKLE_PT_main_panel(bpy.types.Panel):
|
||||
|
||||
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
|
||||
if not context.scene.speckle_state.model_cards:
|
||||
@@ -30,6 +30,11 @@ class SPECKLE_PT_main_panel(bpy.types.Panel):
|
||||
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()
|
||||
@@ -53,30 +58,45 @@ class SPECKLE_PT_main_panel(bpy.types.Panel):
|
||||
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")
|
||||
|
||||
|
||||
# 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"
|
||||
# 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
|
||||
|
||||
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,
|
||||
)
|
||||
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"
|
||||
|
||||
# load button
|
||||
row = layout.row()
|
||||
row.enabled = project_selected and model_selected and version_selected
|
||||
row.operator("speckle.load", text="Load Model", icon="IMPORT")
|
||||
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,
|
||||
)
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import bpy
|
||||
from typing import List
|
||||
from bpy.types import Operator, Context, Object
|
||||
from bpy.props import EnumProperty, StringProperty
|
||||
from bpy.props import EnumProperty
|
||||
|
||||
|
||||
class SPECKLE_OT_selection_filter_dialog(Operator):
|
||||
@@ -11,6 +11,7 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
|
||||
|
||||
bl_idname = "speckle.selection_filter_dialog"
|
||||
bl_label = "Select Objects"
|
||||
bl_description = "Select objects to publish"
|
||||
|
||||
selection_type: EnumProperty(
|
||||
name="Selection",
|
||||
@@ -20,44 +21,36 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
|
||||
default="SELECTION",
|
||||
) # type: ignore
|
||||
|
||||
project_name: StringProperty(
|
||||
name="Project Name", description="Name of the selected project", default=""
|
||||
) # type: ignore
|
||||
|
||||
project_id: StringProperty(
|
||||
name="Project ID", description="ID of the selected project", default=""
|
||||
) # type: ignore
|
||||
|
||||
model_name: StringProperty(
|
||||
name="Model Name", description="Name of the selected model", default=""
|
||||
) # type: ignore
|
||||
|
||||
model_id: StringProperty(
|
||||
name="Model ID", description="ID of the selected model", default=""
|
||||
) # type: ignore
|
||||
|
||||
def execute(self, context: Context) -> set:
|
||||
model_card = context.scene.speckle_state.model_cards.add()
|
||||
model_card.project_name = self.project_name
|
||||
model_card.model_name = self.model_name
|
||||
model_card.model_id = self.model_id
|
||||
model_card.project_id = self.project_id
|
||||
model_card.is_publish = True
|
||||
# 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
|
||||
|
||||
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
|
||||
# selected_objects: list[Object] = context.selected_objects
|
||||
# total_selected: int = len(selected_objects)
|
||||
# object_types: dict[str, int] = {}
|
||||
# for obj in selected_objects:
|
||||
# if obj.type not in object_types:
|
||||
# object_types[obj.type] = 1
|
||||
# else:
|
||||
# object_types[obj.type] += 1
|
||||
|
||||
summary: str = f"{total_selected} objects - "
|
||||
for obj_type, count in object_types.items():
|
||||
summary += f"{obj_type}: {count}, "
|
||||
# summary: str = f"{total_selected} objects - "
|
||||
# for obj_type, count in object_types.items():
|
||||
# summary += f"{obj_type}: {count}, "
|
||||
|
||||
model_card.selection_summary = summary.strip()
|
||||
# model_card.selection_summary = summary.strip()
|
||||
#TODO: implement selection filter dialog
|
||||
wm = context.window_manager
|
||||
wm.speckle_objects.clear()
|
||||
user_selection = context.selected_objects
|
||||
for sel in user_selection:
|
||||
obj = wm.speckle_objects.add()
|
||||
obj.name = sel.name
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, event: bpy.types.Event) -> set:
|
||||
@@ -65,9 +58,10 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
|
||||
|
||||
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}")
|
||||
layout.label(text=f"Project: {wm.selected_project_name}")
|
||||
layout.label(text=f"Model: {wm.selected_model_name}")
|
||||
|
||||
layout.prop(self, "selection_type")
|
||||
layout.separator()
|
||||
@@ -115,3 +109,10 @@ class SPECKLE_OT_selection_filter_dialog(Operator):
|
||||
|
||||
def check(self, context: Context) -> bool:
|
||||
return True # this forces the dialog to redraw
|
||||
|
||||
class speckle_object(bpy.types.PropertyGroup):
|
||||
"""
|
||||
PropertyGroup for storing model information
|
||||
"""
|
||||
|
||||
name: bpy.props.StringProperty() #type: ignore
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import bpy
|
||||
from specklepy.api.credentials import get_local_accounts
|
||||
from specklepy.core.api.credentials import get_local_accounts
|
||||
from typing import List, Tuple, Optional
|
||||
from specklepy.core.api.credentials import Account
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.wrapper import StreamWrapper
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.api.wrapper import StreamWrapper
|
||||
|
||||
from .misc import strip_non_ascii
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.credentials import get_local_accounts, Account
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.api.credentials import get_local_accounts, Account
|
||||
from specklepy.core.api.inputs.project_inputs import ProjectModelsFilter
|
||||
from specklepy.core.api.models.current import Model
|
||||
from typing import List, Tuple, Optional
|
||||
@@ -43,7 +43,11 @@ def get_models_for_project(
|
||||
).items
|
||||
|
||||
return [
|
||||
(strip_non_ascii(model.name), model.id, format_relative_time(model.updated_at))
|
||||
(
|
||||
strip_non_ascii(model.name),
|
||||
model.id,
|
||||
format_relative_time(model.updated_at),
|
||||
)
|
||||
for model in models
|
||||
]
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.credentials import get_local_accounts
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.api.credentials import get_local_accounts, Account
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
from specklepy.core.api.credentials import get_local_accounts, Account
|
||||
from typing import List, Tuple, Optional
|
||||
from .misc import format_relative_time, strip_non_ascii
|
||||
from specklepy.core.api.inputs.model_inputs import ModelVersionsFilter
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from ..converter.to_native import * #noqa: F403
|
||||
from ..converter.to_speckle import * #noqa: F403
|
||||
from ..converter.utils import * # noqa: F403
|
||||
|
||||
@@ -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,49 @@
|
||||
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",
|
||||
) -> Optional[BlenderObject]:
|
||||
display_value = []
|
||||
properties = {}
|
||||
|
||||
if blender_object.type == "CURVE":
|
||||
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":
|
||||
meshes = mesh_to_speckle_meshes(
|
||||
blender_object, blender_object.data, scale_factor, units
|
||||
)
|
||||
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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -5,6 +5,7 @@ from specklepy.objects import Base
|
||||
from specklepy.objects.graph_traversal.default_traversal import (
|
||||
create_default_traversal_function,
|
||||
)
|
||||
from specklepy.core.api.client import SpeckleClient
|
||||
|
||||
|
||||
def to_rgba(argb_int: int) -> Tuple[float, float, float, float]:
|
||||
@@ -59,13 +60,11 @@ def create_material_from_proxy(
|
||||
1.0,
|
||||
)
|
||||
|
||||
|
||||
if hasattr(render_material, "opacity"):
|
||||
opacity = float(render_material.opacity)
|
||||
if opacity < 1.0:
|
||||
material.blend_method = "BLEND"
|
||||
bsdf.inputs["Alpha"].default_value = opacity
|
||||
|
||||
|
||||
if hasattr(render_material, "metalness"):
|
||||
metalness = float(render_material.metalness)
|
||||
@@ -88,10 +87,15 @@ def create_material_from_proxy(
|
||||
1.0,
|
||||
)
|
||||
bsdf.inputs["Emission Strength"].default_value = 1.0
|
||||
|
||||
|
||||
# set viewport display color
|
||||
if hasattr(render_material, "diffuse") and hasattr(render_material, "opacity"):
|
||||
material.diffuse_color = (diffuse_rgba[0], diffuse_rgba[1], diffuse_rgba[2], opacity)
|
||||
material.diffuse_color = (
|
||||
diffuse_rgba[0],
|
||||
diffuse_rgba[1],
|
||||
diffuse_rgba[2],
|
||||
opacity,
|
||||
)
|
||||
|
||||
return material
|
||||
|
||||
@@ -183,3 +187,21 @@ def find_object_by_id(root_object: Base, target_id: str) -> Optional[Base]:
|
||||
return None
|
||||
|
||||
return deep_search(root_object)
|
||||
|
||||
|
||||
def get_project_workspace_id(client: SpeckleClient, project_id: str) -> Optional[str]:
|
||||
workspace_id = None
|
||||
server_version = client.project.server_version or client.server.version()
|
||||
|
||||
# Local yarn builds of server will report a server version if "dev"
|
||||
# We'll assume that local builds are up-to-date with the latest features
|
||||
if server_version[0] == "dev":
|
||||
maj = 999
|
||||
min = 999
|
||||
else:
|
||||
maj = server_version[0]
|
||||
min = server_version[1]
|
||||
|
||||
if maj > 2 or (maj == 2 and min > 20):
|
||||
workspace_id = client.project.get(project_id).workspace_id
|
||||
return workspace_id
|
||||
|
||||
+28
-47
@@ -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
@@ -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.0a15",
|
||||
"specklepy==3.0.0a16",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -132,7 +132,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "gql"
|
||||
version = "3.5.2"
|
||||
version = "3.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -140,9 +140,9 @@ dependencies = [
|
||||
{ name = "graphql-core" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/49/ef/5298d9d628b6a54b3b810052cb5a935d324fe28d9bfdeb741733d5c2446b/gql-3.5.2.tar.gz", hash = "sha256:07e1325b820c8ba9478e95de27ce9f23250486e7e79113dbb7659a442dc13e74", size = 180502 }
|
||||
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/ff/71/b028b937992056e721bbf0371e13819fcca0dacde7b3c821f775ed903917/gql-3.5.2-py2.py3-none-any.whl", hash = "sha256:c830ffc38b3997b2a146317b27758305ab3d0da3bde607b49f34e32affb23ba2", size = 74346 },
|
||||
{ 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]
|
||||
@@ -156,11 +156,11 @@ websockets = [
|
||||
|
||||
[[package]]
|
||||
name = "graphql-core"
|
||||
version = "3.2.4"
|
||||
version = "3.2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/9e/aa527fb09a9d7399d5d7d2aa2da490e4580707652d3b4fc156996ae88a5b/graphql-core-3.2.4.tar.gz", hash = "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264", size = 504611 }
|
||||
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/d1/33/cc72c4c658c6316f188a60bc4e5a91cd4ceaaa8c3e7e691ac9297e4e72c7/graphql_core-3.2.4-py3-none-any.whl", hash = "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0", size = 203179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -211,79 +211,79 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.4.3"
|
||||
version = "6.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372 }
|
||||
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/16/e0/53cf7f27eda48fffa53cfd4502329ed29e00efb9e4ce41362cbf8aa54310/multidict-6.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f6f19170197cc29baccd33ccc5b5d6a331058796485857cf34f7635aa25fb0cd", size = 65259 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/79/1dcd93ce7070cf01c2ee29f781c42b33c64fce20033808f1cc9ec8413d6e/multidict-6.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2882bf27037eb687e49591690e5d491e677272964f9ec7bc2abbe09108bdfb8", size = 38451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/35/2292cf29ab5f0d0b3613fad1b75692148959d3834d806be1885ceb49a8ff/multidict-6.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fbf226ac85f7d6b6b9ba77db4ec0704fde88463dc17717aec78ec3c8546c70ad", size = 37706 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d1/6b157110b2b187b5a608b37714acb15ee89ec773e3800315b0107ea648cd/multidict-6.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e329114f82ad4b9dd291bef614ea8971ec119ecd0f54795109976de75c9a852", size = 226669 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/7f/61a476450651f177c5570e04bd55947f693077ba7804fe9717ee9ae8de04/multidict-6.4.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1f4e0334d7a555c63f5c8952c57ab6f1c7b4f8c7f3442df689fc9f03df315c08", size = 223182 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/7b/eaf7502ac4824cdd8edcf5723e2e99f390c879866aec7b0c420267b53749/multidict-6.4.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:740915eb776617b57142ce0bb13b7596933496e2f798d3d15a20614adf30d229", size = 235025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/f6/facdbbd73c96b67a93652774edd5778ab1167854fa08ea35ad004b1b70ad/multidict-6.4.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255dac25134d2b141c944b59a0d2f7211ca12a6d4779f7586a98b4b03ea80508", size = 231481 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/57/c008e861b3052405eebf921fd56a748322d8c44dcfcab164fffbccbdcdc4/multidict-6.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4e8535bd4d741039b5aad4285ecd9b902ef9e224711f0b6afda6e38d7ac02c7", size = 223492 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/4d/7d8440d3a12a6ae5d6b202d6e7f2ac6ab026e04e99aaf1b73f18e6bc34bc/multidict-6.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c433a33be000dd968f5750722eaa0991037be0be4a9d453eba121774985bc8", size = 217279 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/e7/bca0df4dd057597b94138d2d8af04eb3c27396a425b1b0a52e082f9be621/multidict-6.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4eb33b0bdc50acd538f45041f5f19945a1f32b909b76d7b117c0c25d8063df56", size = 228733 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f5/383827c3f1c38d7c92dbad00a8a041760228573b1c542fbf245c37bbca8a/multidict-6.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:75482f43465edefd8a5d72724887ccdcd0c83778ded8f0cb1e0594bf71736cc0", size = 218089 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/8a/a5174e8a7d8b94b4c8f9c1e2cf5d07451f41368ffe94d05fc957215b8e72/multidict-6.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce5b3082e86aee80b3925ab4928198450d8e5b6466e11501fe03ad2191c6d777", size = 225257 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/76/1d4b7218f0fd00b8e5c90b88df2e45f8af127f652f4e41add947fa54c1c4/multidict-6.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e413152e3212c4d39f82cf83c6f91be44bec9ddea950ce17af87fbf4e32ca6b2", size = 234728 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/44/18372a4f6273fc7ca25630d7bf9ae288cde64f29593a078bff450c7170b6/multidict-6.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8aac2eeff69b71f229a405c0a4b61b54bade8e10163bc7b44fcd257949620618", size = 230087 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/ae/28728c314a698d8a6d9491fcacc897077348ec28dd85884d09e64df8a855/multidict-6.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ab583ac203af1d09034be41458feeab7863c0635c650a16f15771e1386abf2d7", size = 223137 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/50/785bb2b3fe16051bc91c70a06a919f26312da45c34db97fc87441d61e343/multidict-6.4.3-cp311-cp311-win32.whl", hash = "sha256:1b2019317726f41e81154df636a897de1bfe9228c3724a433894e44cd2512378", size = 34959 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/63/2a22e099ae2f4d92897618c00c73a09a08a2a9aa14b12736965bf8d59fd3/multidict-6.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:43173924fa93c7486402217fab99b60baf78d33806af299c56133a3755f69589", size = 38541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/bb/3abdaf8fe40e9226ce8a2ba5ecf332461f7beec478a455d6587159f1bf92/multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", size = 64019 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/b5/1b2e8de8217d2e89db156625aa0fe4a6faad98972bfe07a7b8c10ef5dd6b/multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", size = 37925 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/e2/3ca91c112644a395c8eae017144c907d173ea910c913ff8b62549dcf0bbf/multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", size = 37008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/23/79bc78146c7ac8d1ac766b2770ca2e07c2816058b8a3d5da6caed8148637/multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", size = 224374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/35/77950ed9ebd09136003a85c1926ba42001ca5be14feb49710e4334ee199b/multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", size = 230869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/97/2a33c6e7d90bc116c636c14b2abab93d6521c0c052d24bfcc231cbf7f0e7/multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", size = 231949 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/ce/e9b5d9fcf854f61d6686ada7ff64893a7a5523b2a07da6f1265eaaea5151/multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", size = 231032 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ac/7ced59dcdfeddd03e601edb05adff0c66d81ed4a5160c443e44f2379eef0/multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", size = 223517 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/e6/325ed9055ae4e085315193a1b58bdb4d7fc38ffcc1f4975cfca97d015e17/multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", size = 216291 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/84/eeee6d477dd9dcb7691c3bb9d08df56017f5dd15c730bcc9383dcf201cf4/multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", size = 228982 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/94/4d1f3e74e7acf8b0c85db350e012dcc61701cd6668bc2440bb1ecb423c90/multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", size = 226823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/f0/1e54b95bda7cd01080e5732f9abb7b76ab5cc795b66605877caeb2197476/multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", size = 222714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/a2/f6cbca875195bd65a3e53b37ab46486f3cc125bdeab20eefe5042afa31fb/multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", size = 233739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/68/9891f4d2b8569554723ddd6154375295f789dc65809826c6fb96a06314fd/multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", size = 230809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/72/a7be29ba1e87e4fc5ceb44dabc7940b8005fd2436a332a23547709315f70/multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", size = 226934 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/c1/259386a9ad6840ff7afc686da96808b503d152ac4feb3a96c651dc4f5abf/multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", size = 35242 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/24/c8fdff4f924d37225dc0c56a28b1dca10728fc2233065fafeb27b4b125be/multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", size = 38635 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/4b/86fd786d03915c6f49998cf10cd5fe6b6ac9e9a071cb40885d2e080fb90d/multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", size = 63831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/05/9b51fdf7aef2563340a93be0a663acba2c428c4daeaf3960d92d53a4a930/multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", size = 37888 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/43/53fc25394386c911822419b522181227ca450cf57fea76e6188772a1bd91/multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", size = 36852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/68/7b99c751e822467c94a235b810a2fd4047d4ecb91caef6b5c60116991c4b/multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", size = 223644 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1b/d458d791e4dd0f7e92596667784fbf99e5c8ba040affe1ca04f06b93ae92/multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", size = 230446 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/46/9793378d988905491a7806d8987862dc5a0bae8a622dd896c4008c7b226b/multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", size = 231070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/b8/b127d3e1f8dd2a5bf286b47b24567ae6363017292dc6dec44656e6246498/multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", size = 229956 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/93/f70a4c35b103fcfe1443059a2bb7f66e5c35f2aea7804105ff214f566009/multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", size = 222599 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/8c/e28e0eb2fe34921d6aa32bfc4ac75b09570b4d6818cc95d25499fe08dc1d/multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", size = 216136 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f5/fbc81f866585b05f89f99d108be5d6ad170e3b6c4d0723d1a2f6ba5fa918/multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", size = 228139 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ba/7d196bad6b85af2307d81f6979c36ed9665f49626f66d883d6c64d156f78/multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", size = 226251 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/e2/fae46a370dce79d08b672422a33df721ec8b80105e0ea8d87215ff6b090d/multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", size = 221868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/20/bbc9a3dec19d5492f54a167f08546656e7aef75d181d3d82541463450e88/multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", size = 233106 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8d/f30ae8f5ff7a2461177f4d8eb0d8f69f27fb6cfe276b54ec4fd5a282d918/multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", size = 230163 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/e9/2833f3c218d3c2179f3093f766940ded6b81a49d2e2f9c46ab240d23dfec/multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", size = 225906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/31/6edab296ac369fd286b845fa5dd4c409e63bc4655ed8c9510fcb477e9ae9/multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", size = 35238 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/57/2c0167a1bffa30d9a1383c3dab99d8caae985defc8636934b5668830d2ef/multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", size = 38799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/13/2ead63b9ab0d2b3080819268acb297bd66e238070aa8d42af12b08cbee1c/multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", size = 68642 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/45/f1a751e1eede30c23951e2ae274ce8fad738e8a3d5714be73e0a41b27b16/multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", size = 40028 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/29/fcc53e886a2cc5595cc4560df333cb9630257bda65003a7eb4e4e0d8f9c1/multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", size = 39424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f0/056c81119d8b88703971f937b371795cab1407cd3c751482de5bfe1a04a9/multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", size = 226178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/79/3b7e5fea0aa80583d3a69c9d98b7913dfd4fbc341fb10bb2fb48d35a9c21/multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", size = 222617 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/db/3ed012b163e376fc461e1d6a67de69b408339bc31dc83d39ae9ec3bf9578/multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", size = 227919 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/db/0433c104bca380989bc04d3b841fc83e95ce0c89f680e9ea4251118b52b6/multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", size = 226097 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/910db2618175724dd254b7ae635b6cd8d2947a8b76b0376de7b96d814dab/multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", size = 220706 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/af/aa176c6f5f1d901aac957d5258d5e22897fe13948d1e69063ae3d5d0ca01/multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", size = 211728 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/42/d51cc5fc1527c3717d7f85137d6c79bb7a93cd214c26f1fc57523774dbb5/multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", size = 226276 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/6b/d836dea45e0b8432343ba4acf9a8ecaa245da4c0960fb7ab45088a5e568a/multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", size = 212069 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/34/0ee1a7adb3560e18ee9289c6e5f7db54edc312b13e5c8263e88ea373d12c/multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", size = 217858 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/08/586d652c2f5acefe0cf4e658eedb4d71d4ba6dfd4f189bd81b400fc1bc6b/multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", size = 226988 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/e3/cc59c7e2bc49d7f906fb4ffb6d9c3a3cf21b9f2dd9c96d05bef89c2b1fd1/multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", size = 220435 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/32/5c3a556118aca9981d883f38c4b1bfae646f3627157f70f4068e5a648955/multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", size = 221494 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/3b/1599631f59024b75c4d6e3069f4502409970a336647502aaf6b62fb7ac98/multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", size = 41775 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4e/09301668d675d02ca8e8e1a3e6be046619e30403f5ada2ed5b080ae28d02/multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", size = 45946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400 },
|
||||
{ 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]]
|
||||
@@ -361,7 +361,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.4"
|
||||
version = "2.11.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
@@ -369,9 +369,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/77/ab/5250d56ad03884ab5efd07f734203943c8a8ab40d551e208af81d0257bf2/pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d", size = 786540 }
|
||||
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/e7/12/46b65f3534d099349e38ef6ec98b1a5a81f42536d17e0ba382c28c67ba67/pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb", size = 443900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -537,7 +537,7 @@ dev = [
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "specklepy", specifier = "==3.0.0a15" }]
|
||||
requires-dist = [{ name = "specklepy", specifier = "==3.0.0a16" }]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
@@ -547,7 +547,7 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "specklepy"
|
||||
version = "3.0.0a15"
|
||||
version = "3.0.0a16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "appdirs" },
|
||||
@@ -559,9 +559,9 @@ dependencies = [
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "ujson" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/00/ef793063b92c4e998bae227cf3a94c640181ac63f385a5be2b15ff806f0d/specklepy-3.0.0a15.tar.gz", hash = "sha256:60bfce016d58e31f7f4b5b8bb61cc8528dc0404b1246fac96bc961ee46cb0816", size = 199633 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/8f/8730b78c6e669117d7c97489e47bd8b75b93aabadc03020dd2aad7ab198c/specklepy-3.0.0a16.tar.gz", hash = "sha256:3f5949758cbc8b30d7b1ec21094e0457be4491a90dd2450bc9c15f3b40cbf5cc", size = 200721 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f5/a8acbfa11b9d8a02baf291fd36ad67d495f90c862b510133f88e7cc59861/specklepy-3.0.0a15-py3-none-any.whl", hash = "sha256:129816063ef692e8566e95e8201b3dabfb8835a67aa91d05bd6d2b10ed8449fa", size = 100948 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/59/895e4a980a75c27d39c2cf453bb16af54b70a8d1e75d6ed4e2ac960747e4/specklepy-3.0.0a16-py3-none-any.whl", hash = "sha256:919feae14e319524c89063c22fd34aaa45deafb3d8c8b3f06094c82af4873e8c", size = 101009 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -575,14 +575,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
|
||||
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/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user