From 7370bc534a89e4a4516a09a42a723dac4024a5e6 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 1 Apr 2025 02:20:40 +0800 Subject: [PATCH 1/4] feat: Add Geometry Node Module --- .gitignore | 2 + README.md | 12 + addon.py | 1523 +++++++++++++++++++++++++++++++------ src/blender_mcp/server.py | 237 +++++- 4 files changed, 1524 insertions(+), 250 deletions(-) diff --git a/.gitignore b/.gitignore index 505a3b1ca..6ec78364a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ wheels/ # Virtual environments .venv + +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 116cce7c4..29de15c9f 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ Once the config file has been set on Claude, and the addon is running on Blender - Execute any Python code in Blender - Download the right models, assets and HDRIs through [Poly Haven](https://polyhaven.com/) - AI generated 3D models through [Hyper3D Rodin](https://hyper3d.ai/) +- Geometry Node Module To Create procedural modeling and geometry generation ### Example Commands @@ -167,10 +168,21 @@ Here are some examples of what you can ask Claude to do: - "Make the lighting like a studio" - "Point the camera at the scene, and make it isometric" + +## Geometry Node Module Example Commands + +- "Create a simple procedural table" +- "Create a simple procedural chair" +- "Create a simple procedural car" +- "Use geometry nodes to create points distributed on a sphere surface with exposed radius and density parameters" +- "Use geometry nodes to create a noise-deformed sphere with exposed strength parameter" + + ## Hyper3D integration Hyper3D's free trial key allows you to generate a limited number of models per day. If the daily limit is reached, you can wait for the next day's reset or obtain your own key from hyper3d.ai and fal.ai. + ## Troubleshooting - **Connection issues**: Make sure the Blender addon server is running, and the MCP server is configured on Claude, DO NOT run the uvx command in the terminal. Sometimes, the first command won't go through but after that it starts working. diff --git a/addon.py b/addon.py index 65648b109..e6a52fa55 100644 --- a/addon.py +++ b/addon.py @@ -10,6 +10,8 @@ import os import shutil from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty +from dataclasses import dataclass, field +from typing import List, Dict, Union, Any, Optional, Tuple bl_info = { "name": "Blender MCP", @@ -23,6 +25,74 @@ RODIN_FREE_TRIAL_KEY = "k9TcfFoEhNd9cCPP2guHAHHHkctZHIRhZDywZ1euGUXwihbYLpOjQhofby80NJez" +# region GeometryNodeDataClass +IS_BLENDER_4 = bpy.app.version[0] >= 4 + + +@dataclass +class NodeDefinition: + """Node definition data class""" + type: str # Node type name + location: List[float] = field(default_factory=lambda: [0.0, 0.0]) # Node position [x, y] + label: str = "" # Node label + inputs: Dict[str, Any] = field(default_factory=dict) # Input values dictionary + properties: Dict[str, Any] = field(default_factory=dict) # Node properties parameter dictionary + + +@dataclass +class NodeLink: + """Node connection data class""" + from_node: Union[str, int] # Source node name or index + from_socket: Union[str, int] # Source socket name or index + to_node: Union[str, int] # Target node name or index + to_socket: Union[str, int] # Target socket name or index + + +@dataclass +class GeometryNodeNetwork: + """Geometry node network data class""" + object_name: str # Object name + nodes: List[NodeDefinition] = field(default_factory=list) # Node list + links: List[NodeLink] = field(default_factory=list) # Connection list + input_sockets: List[Dict[str, str]] = field(default_factory=list) # Input interface definition + output_sockets: List[Dict[str, str]] = field(default_factory=list) # Output interface definition + + +@dataclass +class SocketInfo: + """Socket information data class""" + name: str # Socket name + type: str # Socket type + description: str # Socket description + identifier: str # Socket identifier + enabled: bool # Whether enabled + hide: bool # Whether hidden + default_value: Any = None # Default value (if any) + + +@dataclass +class PropertyInfo: + """Node property information data class""" + identifier: str # Property identifier + name: str # Property name + description: str # Property description + type: str # Property type + default_value: Any = None # Default value (if any) + enum_items: List[Dict[str, str]] = field(default_factory=list) # Enum options (if any) + + +@dataclass +class NodeInfo: + """Node information data class""" + name: str # Node type name (identifier used to create the node) + description: str # Node description + inputs: List[SocketInfo] = field(default_factory=list) # Input socket information + outputs: List[SocketInfo] = field(default_factory=list) # Output socket information + properties: List[PropertyInfo] = field(default_factory=list) # Node property information + + +# endregion + class BlenderMCPServer: def __init__(self, host='localhost', port=9876): self.host = host @@ -30,34 +100,34 @@ def __init__(self, host='localhost', port=9876): self.running = False self.socket = None self.server_thread = None - + def start(self): if self.running: print("Server is already running") return - + self.running = True - + try: # Create socket self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.socket.bind((self.host, self.port)) self.socket.listen(1) - + # Start server thread self.server_thread = threading.Thread(target=self._server_loop) self.server_thread.daemon = True self.server_thread.start() - + print(f"BlenderMCP server started on {self.host}:{self.port}") except Exception as e: print(f"Failed to start server: {str(e)}") self.stop() - + def stop(self): self.running = False - + # Close socket if self.socket: try: @@ -65,7 +135,7 @@ def stop(self): except: pass self.socket = None - + # Wait for thread to finish if self.server_thread: try: @@ -74,21 +144,21 @@ def stop(self): except: pass self.server_thread = None - + print("BlenderMCP server stopped") - + def _server_loop(self): """Main server loop in a separate thread""" print("Server thread started") self.socket.settimeout(1.0) # Timeout to allow for stopping - + while self.running: try: # Accept new connection try: client, address = self.socket.accept() print(f"Connected to client: {address}") - + # Handle client in a separate thread client_thread = threading.Thread( target=self._handle_client, @@ -107,15 +177,15 @@ def _server_loop(self): if not self.running: break time.sleep(0.5) - + print("Server thread stopped") - + def _handle_client(self, client): """Handle connected client""" print("Client handler started") client.settimeout(None) # No timeout buffer = b'' - + try: while self.running: # Receive data @@ -124,13 +194,13 @@ def _handle_client(self, client): if not data: print("Client disconnected") break - + buffer += data try: # Try to parse command command = json.loads(buffer.decode('utf-8')) buffer = b'' - + # Execute command in Blender's main thread def execute_wrapper(): try: @@ -152,7 +222,7 @@ def execute_wrapper(): except: pass return None - + # Schedule execution in main thread bpy.app.timers.register(execute_wrapper, first_interval=0.0) except json.JSONDecodeError: @@ -175,7 +245,7 @@ def execute_command(self, command): try: cmd_type = command.get("type") params = command.get("params", {}) - + # Ensure we're in the right context if cmd_type in ["create_object", "modify_object", "delete_object"]: override = bpy.context.copy() @@ -184,7 +254,7 @@ def execute_command(self, command): return self._execute_command_internal(command) else: return self._execute_command_internal(command) - + except Exception as e: print(f"Error executing command: {str(e)}") traceback.print_exc() @@ -198,7 +268,15 @@ def _execute_command_internal(self, command): # Add a handler for checking PolyHaven status if cmd_type == "get_polyhaven_status": return {"status": "success", "result": self.get_polyhaven_status()} - + + # Add a handler for checking Hyper3D status + if cmd_type == "get_hyper3d_status": + return {"status": "success", "result": self.get_hyper3d_status()} + + # Add a handler for checking GeometryNodes status + if cmd_type == "get_geometry_nodes_status": + return {"status": "success", "result": self.get_geometry_nodes_status()} + # Base handlers that are always available handlers = { "get_scene_info": self.get_scene_info, @@ -210,8 +288,9 @@ def _execute_command_internal(self, command): "set_material": self.set_material, "get_polyhaven_status": self.get_polyhaven_status, "get_hyper3d_status": self.get_hyper3d_status, + "get_geometry_nodes_status": self.get_geometry_nodes_status, } - + # Add Polyhaven handlers only if enabled if bpy.context.scene.blendermcp_use_polyhaven: polyhaven_handlers = { @@ -221,15 +300,23 @@ def _execute_command_internal(self, command): "set_texture": self.set_texture, } handlers.update(polyhaven_handlers) - + # Add Hyper3d handlers only if enabled if bpy.context.scene.blendermcp_use_hyper3d: - polyhaven_handlers = { + hyper3d_handlers = { "create_rodin_job": self.create_rodin_job, "poll_rodin_job_status": self.poll_rodin_job_status, "import_generated_asset": self.import_generated_asset, } - handlers.update(polyhaven_handlers) + handlers.update(hyper3d_handlers) + + # Add geometry nodes handlers only if enabled + if bpy.context.scene.blendermcp_use_geometry_nodes: + geometry_nodes_handlers = { + "get_node_info": self.get_node_info, + "complete_geometry_node": self.complete_geometry_node, + } + handlers.update(geometry_nodes_handlers) handler = handlers.get(cmd_type) if handler: @@ -245,7 +332,6 @@ def _execute_command_internal(self, command): else: return {"status": "error", "message": f"Unknown command type: {cmd_type}"} - def get_simple_info(self): """Get basic Blender information""" return { @@ -253,7 +339,7 @@ def get_simple_info(self): "scene_name": bpy.context.scene.name, "object_count": len(bpy.context.scene.objects) } - + def get_scene_info(self): """Get information about the current Blender scene""" try: @@ -265,29 +351,29 @@ def get_scene_info(self): "objects": [], "materials_count": len(bpy.data.materials), } - + # Collect minimal object information (limit to first 10 objects) for i, obj in enumerate(bpy.context.scene.objects): if i >= 10: # Reduced from 20 to 10 break - + obj_info = { "name": obj.name, "type": obj.type, # Only include basic location data - "location": [round(float(obj.location.x), 2), - round(float(obj.location.y), 2), - round(float(obj.location.z), 2)], + "location": [round(float(obj.location.x), 2), + round(float(obj.location.y), 2), + round(float(obj.location.z), 2)], } scene_info["objects"].append(obj_info) - + print(f"Scene info collected: {len(scene_info['objects'])} objects") return scene_info except Exception as e: print(f"Error in get_scene_info: {str(e)}") traceback.print_exc() return {"error": str(e)} - + @staticmethod def _get_aabb(obj): """ Returns the world-space axis-aligned bounding box (AABB) of an object. """ @@ -309,13 +395,13 @@ def _get_aabb(obj): ] def create_object(self, type="CUBE", name=None, location=(0, 0, 0), rotation=(0, 0, 0), scale=(1, 1, 1), - align="WORLD", major_segments=48, minor_segments=12, mode="MAJOR_MINOR", - major_radius=1.0, minor_radius=0.25, abso_major_rad=1.25, abso_minor_rad=0.75, generate_uvs=True): + align="WORLD", major_segments=48, minor_segments=12, mode="MAJOR_MINOR", + major_radius=1.0, minor_radius=0.25, abso_major_rad=1.25, abso_minor_rad=0.75, generate_uvs=True): """Create a new object in the scene""" try: # Deselect all objects first bpy.ops.object.select_all(action='DESELECT') - + # Create the object based on type if type == "CUBE": bpy.ops.mesh.primitive_cube_add(location=location, rotation=rotation, scale=scale) @@ -349,26 +435,26 @@ def create_object(self, type="CUBE", name=None, location=(0, 0, 0), rotation=(0, bpy.ops.object.light_add(type='POINT', location=location, rotation=rotation, scale=scale) else: raise ValueError(f"Unsupported object type: {type}") - + # Force update the view layer bpy.context.view_layer.update() - + # Get the active object (which should be our newly created object) obj = bpy.context.view_layer.objects.active - + # If we don't have an active object, something went wrong if obj is None: raise RuntimeError("Failed to create object - no active object") - + # Make sure it's selected obj.select_set(True) - + # Rename if name is provided if name: obj.name = name if obj.data: obj.data.name = name - + # Patch for PLANE: scale don't work with bpy.ops.mesh.primitive_plane_add() if type in {"PLANE"}: obj.scale = scale @@ -381,11 +467,11 @@ def create_object(self, type="CUBE", name=None, location=(0, 0, 0), rotation=(0, "rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z], "scale": [obj.scale.x, obj.scale.y, obj.scale.z], } - + if obj.type == "MESH": bounding_box = self._get_aabb(obj) result["world_bounding_box"] = bounding_box - + return result except Exception as e: print(f"Error in create_object: {str(e)}") @@ -398,21 +484,21 @@ def modify_object(self, name, location=None, rotation=None, scale=None, visible= obj = bpy.data.objects.get(name) if not obj: raise ValueError(f"Object not found: {name}") - + # Modify properties as requested if location is not None: obj.location = location - + if rotation is not None: obj.rotation_euler = rotation - + if scale is not None: obj.scale = scale - + if visible is not None: obj.hide_viewport = not visible obj.hide_render = not visible - + result = { "name": obj.name, "type": obj.type, @@ -433,22 +519,22 @@ def delete_object(self, name): obj = bpy.data.objects.get(name) if not obj: raise ValueError(f"Object not found: {name}") - + # Store the name to return obj_name = obj.name - + # Select and delete the object if obj: bpy.data.objects.remove(obj, do_unlink=True) - + return {"deleted": obj_name} - + def get_object_info(self, name): """Get detailed information about a specific object""" obj = bpy.data.objects.get(name) if not obj: raise ValueError(f"Object not found: {name}") - + # Basic object info obj_info = { "name": obj.name, @@ -463,12 +549,12 @@ def get_object_info(self, name): if obj.type == "MESH": bounding_box = self._get_aabb(obj) obj_info["world_bounding_box"] = bounding_box - + # Add material slots for slot in obj.material_slots: if slot.material: obj_info["materials"].append(slot.material.name) - + # Add mesh data if applicable if obj.type == 'MESH' and obj.data: mesh = obj.data @@ -477,9 +563,9 @@ def get_object_info(self, name): "edges": len(mesh.edges), "polygons": len(mesh.polygons), } - + return obj_info - + def execute_code(self, code): """Execute arbitrary Blender Python code""" # This is powerful but potentially dangerous - use with caution @@ -490,7 +576,7 @@ def execute_code(self, code): return {"executed": True} except Exception as e: raise Exception(f"Code execution error: {str(e)}") - + def set_material(self, object_name, material_name=None, create_if_missing=True, color=None): """Set or create a material for an object""" try: @@ -498,11 +584,11 @@ def set_material(self, object_name, material_name=None, create_if_missing=True, obj = bpy.data.objects.get(object_name) if not obj: raise ValueError(f"Object not found: {object_name}") - + # Make sure object can accept materials if not hasattr(obj, 'data') or not hasattr(obj.data, 'materials'): raise ValueError(f"Object {object_name} cannot accept materials") - + # Create or get material if material_name: mat = bpy.data.materials.get(material_name) @@ -517,12 +603,12 @@ def set_material(self, object_name, material_name=None, create_if_missing=True, mat = bpy.data.materials.new(name=mat_name) material_name = mat_name print(f"Using material: {mat_name}") - + # Set up material nodes if needed if mat: if not mat.use_nodes: mat.use_nodes = True - + # Get or create Principled BSDF principled = mat.node_tree.nodes.get('Principled BSDF') if not principled: @@ -534,7 +620,7 @@ def set_material(self, object_name, material_name=None, create_if_missing=True, # Link if not already linked if not principled.outputs[0].links: mat.node_tree.links.new(principled.outputs[0], output.inputs[0]) - + # Set color if provided if color and len(color) >= 3: principled.inputs['Base Color'].default_value = ( @@ -544,7 +630,7 @@ def set_material(self, object_name, material_name=None, create_if_missing=True, 1.0 if len(color) < 4 else color[3] ) print(f"Set material color to {color}") - + # Assign material to object if not already assigned if mat: if not obj.data.materials: @@ -552,9 +638,9 @@ def set_material(self, object_name, material_name=None, create_if_missing=True, else: # Only modify first material slot obj.data.materials[0] = mat - + print(f"Assigned material {mat.name} to object {object_name}") - + return { "status": "success", "object": object_name, @@ -563,7 +649,7 @@ def set_material(self, object_name, material_name=None, create_if_missing=True, } else: raise ValueError(f"Failed to create or find material: {material_name}") - + except Exception as e: print(f"Error in set_material: {str(e)}") traceback.print_exc() @@ -573,21 +659,21 @@ def set_material(self, object_name, material_name=None, create_if_missing=True, "object": object_name, "material": material_name if 'material_name' in locals() else None } - + def render_scene(self, output_path=None, resolution_x=None, resolution_y=None): """Render the current scene""" if resolution_x is not None: bpy.context.scene.render.resolution_x = resolution_x - + if resolution_y is not None: bpy.context.scene.render.resolution_y = resolution_y - + if output_path: bpy.context.scene.render.filepath = output_path - + # Render the scene bpy.ops.render.render(write_still=bool(output_path)) - + return { "rendered": True, "output_path": output_path if output_path else "[not saved]", @@ -599,7 +685,7 @@ def get_polyhaven_categories(self, asset_type): try: if asset_type not in ["hdris", "textures", "models", "all"]: return {"error": f"Invalid asset type: {asset_type}. Must be one of: hdris, textures, models, all"} - + response = requests.get(f"https://api.polyhaven.com/categories/{asset_type}") if response.status_code == 200: return {"categories": response.json()} @@ -607,21 +693,21 @@ def get_polyhaven_categories(self, asset_type): return {"error": f"API request failed with status code {response.status_code}"} except Exception as e: return {"error": str(e)} - + def search_polyhaven_assets(self, asset_type=None, categories=None): """Search for assets from Polyhaven with optional filtering""" try: url = "https://api.polyhaven.com/assets" params = {} - + if asset_type and asset_type != "all": if asset_type not in ["hdris", "textures", "models"]: return {"error": f"Invalid asset type: {asset_type}. Must be one of: hdris, textures, models, all"} params["type"] = asset_type - + if categories: params["categories"] = categories - + response = requests.get(url, params=params) if response.status_code == 200: # Limit the response size to avoid overwhelming Blender @@ -632,32 +718,33 @@ def search_polyhaven_assets(self, asset_type=None, categories=None): if i >= 20: # Limit to 20 assets break limited_assets[key] = value - + return {"assets": limited_assets, "total_count": len(assets), "returned_count": len(limited_assets)} else: return {"error": f"API request failed with status code {response.status_code}"} except Exception as e: return {"error": str(e)} - + def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_format=None): try: # First get the files information files_response = requests.get(f"https://api.polyhaven.com/files/{asset_id}") if files_response.status_code != 200: return {"error": f"Failed to get asset files: {files_response.status_code}"} - + files_data = files_response.json() - + # Handle different asset types if asset_type == "hdris": # For HDRIs, download the .hdr or .exr file if not file_format: file_format = "hdr" # Default format for HDRIs - - if "hdri" in files_data and resolution in files_data["hdri"] and file_format in files_data["hdri"][resolution]: + + if "hdri" in files_data and resolution in files_data["hdri"] and file_format in files_data["hdri"][ + resolution]: file_info = files_data["hdri"][resolution][file_format] file_url = file_info["url"] - + # For HDRIs, we need to save to a temporary file first # since Blender can't properly load HDR data directly from memory with tempfile.NamedTemporaryFile(suffix=f".{file_format}", delete=False) as tmp_file: @@ -665,35 +752,35 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f response = requests.get(file_url) if response.status_code != 200: return {"error": f"Failed to download HDRI: {response.status_code}"} - + tmp_file.write(response.content) tmp_path = tmp_file.name - + try: # Create a new world if none exists if not bpy.data.worlds: bpy.data.worlds.new("World") - + world = bpy.data.worlds[0] world.use_nodes = True node_tree = world.node_tree - + # Clear existing nodes for node in node_tree.nodes: node_tree.nodes.remove(node) - + # Create nodes tex_coord = node_tree.nodes.new(type='ShaderNodeTexCoord') tex_coord.location = (-800, 0) - + mapping = node_tree.nodes.new(type='ShaderNodeMapping') mapping.location = (-600, 0) - + # Load the image from the temporary file env_tex = node_tree.nodes.new(type='ShaderNodeTexEnvironment') env_tex.location = (-400, 0) env_tex.image = bpy.data.images.load(tmp_path) - + # Use a color space that exists in all Blender versions if file_format.lower() == 'exr': # Try to use Linear color space for EXR files @@ -710,30 +797,30 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f break # Stop if we successfully set a color space except: continue - + background = node_tree.nodes.new(type='ShaderNodeBackground') background.location = (-200, 0) - + output = node_tree.nodes.new(type='ShaderNodeOutputWorld') output.location = (0, 0) - + # Connect nodes node_tree.links.new(tex_coord.outputs['Generated'], mapping.inputs['Vector']) node_tree.links.new(mapping.outputs['Vector'], env_tex.inputs['Vector']) node_tree.links.new(env_tex.outputs['Color'], background.inputs['Color']) node_tree.links.new(background.outputs['Background'], output.inputs['Surface']) - + # Set as active world bpy.context.scene.world = world - + # Clean up temporary file try: tempfile._cleanup() # This will clean up all temporary files except: pass - + return { - "success": True, + "success": True, "message": f"HDRI {asset_id} imported successfully", "image_name": env_tex.image.name } @@ -741,20 +828,20 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f return {"error": f"Failed to set up HDRI in Blender: {str(e)}"} else: return {"error": f"Requested resolution or format not available for this HDRI"} - + elif asset_type == "textures": if not file_format: file_format = "jpg" # Default format for textures - + downloaded_maps = {} - + try: for map_type in files_data: if map_type not in ["blend", "gltf"]: # Skip non-texture files if resolution in files_data[map_type] and file_format in files_data[map_type][resolution]: file_info = files_data[map_type][resolution][file_format] file_url = file_info["url"] - + # Use NamedTemporaryFile like we do for HDRIs with tempfile.NamedTemporaryFile(suffix=f".{file_format}", delete=False) as tmp_file: # Download the file @@ -762,14 +849,14 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f if response.status_code == 200: tmp_file.write(response.content) tmp_path = tmp_file.name - + # Load image from temporary file image = bpy.data.images.load(tmp_path) image.name = f"{asset_id}_{map_type}.{file_format}" - + # Pack the image into .blend file image.pack() - + # Set color space based on map type if map_type in ['color', 'diffuse', 'albedo']: try: @@ -781,56 +868,56 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f image.colorspace_settings.name = 'Non-Color' except: pass - + downloaded_maps[map_type] = image - + # Clean up temporary file try: os.unlink(tmp_path) except: pass - + if not downloaded_maps: return {"error": f"No texture maps found for the requested resolution and format"} - + # Create a new material with the downloaded textures mat = bpy.data.materials.new(name=asset_id) mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links - + # Clear default nodes for node in nodes: nodes.remove(node) - + # Create output node output = nodes.new(type='ShaderNodeOutputMaterial') output.location = (300, 0) - + # Create principled BSDF node principled = nodes.new(type='ShaderNodeBsdfPrincipled') principled.location = (0, 0) links.new(principled.outputs[0], output.inputs[0]) - + # Add texture nodes based on available maps tex_coord = nodes.new(type='ShaderNodeTexCoord') tex_coord.location = (-800, 0) - + mapping = nodes.new(type='ShaderNodeMapping') mapping.location = (-600, 0) mapping.vector_type = 'TEXTURE' # Changed from default 'POINT' to 'TEXTURE' links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) - + # Position offset for texture nodes x_pos = -400 y_pos = 300 - + # Connect different texture maps for map_type, image in downloaded_maps.items(): tex_node = nodes.new(type='ShaderNodeTexImage') tex_node.location = (x_pos, y_pos) tex_node.image = image - + # Set color space based on map type if map_type.lower() in ['color', 'diffuse', 'albedo']: try: @@ -842,9 +929,9 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f tex_node.image.colorspace_settings.name = 'Non-Color' except: pass # Use default if Non-Color not available - + links.new(mapping.outputs['Vector'], tex_node.inputs['Vector']) - + # Connect to appropriate input on Principled BSDF if map_type.lower() in ['color', 'diffuse', 'albedo']: links.new(tex_node.outputs['Color'], principled.inputs['Base Color']) @@ -864,54 +951,54 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f disp_node.location = (x_pos + 200, y_pos - 200) links.new(tex_node.outputs['Color'], disp_node.inputs['Height']) links.new(disp_node.outputs['Displacement'], output.inputs['Displacement']) - + y_pos -= 250 - + return { - "success": True, + "success": True, "message": f"Texture {asset_id} imported as material", "material": mat.name, "maps": list(downloaded_maps.keys()) } - + except Exception as e: return {"error": f"Failed to process textures: {str(e)}"} - + elif asset_type == "models": # For models, prefer glTF format if available if not file_format: file_format = "gltf" # Default format for models - + if file_format in files_data and resolution in files_data[file_format]: file_info = files_data[file_format][resolution][file_format] file_url = file_info["url"] - + # Create a temporary directory to store the model and its dependencies temp_dir = tempfile.mkdtemp() main_file_path = "" - + try: # Download the main model file main_file_name = file_url.split("/")[-1] main_file_path = os.path.join(temp_dir, main_file_name) - + response = requests.get(file_url) if response.status_code != 200: return {"error": f"Failed to download model: {response.status_code}"} - + with open(main_file_path, "wb") as f: f.write(response.content) - + # Check for included files and download them if "include" in file_info and file_info["include"]: for include_path, include_info in file_info["include"].items(): # Get the URL for the included file - this is the fix include_url = include_info["url"] - + # Create the directory structure for the included file include_file_path = os.path.join(temp_dir, include_path) os.makedirs(os.path.dirname(include_file_path), exist_ok=True) - + # Download the included file include_response = requests.get(include_url) if include_response.status_code == 200: @@ -919,7 +1006,7 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f f.write(include_response.content) else: print(f"Failed to download included file: {include_path}") - + # Import the model into Blender if file_format == "gltf" or file_format == "glb": bpy.ops.import_scene.gltf(filepath=main_file_path) @@ -931,19 +1018,19 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f # For blend files, we need to append or link with bpy.data.libraries.load(main_file_path, link=False) as (data_from, data_to): data_to.objects = data_from.objects - + # Link the objects to the scene for obj in data_to.objects: if obj is not None: bpy.context.collection.objects.link(obj) else: return {"error": f"Unsupported model format: {file_format}"} - + # Get the names of imported objects imported_objects = [obj.name for obj in bpy.context.selected_objects] - + return { - "success": True, + "success": True, "message": f"Model {asset_id} imported successfully", "imported_objects": imported_objects } @@ -957,10 +1044,10 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f print(f"Failed to clean up temporary directory: {temp_dir}") else: return {"error": f"Requested format or resolution not available for this model"} - + else: return {"error": f"Unsupported asset type: {asset_type}"} - + except Exception as e: return {"error": f"Failed to download asset: {str(e)}"} @@ -971,21 +1058,21 @@ def set_texture(self, object_name, texture_id): obj = bpy.data.objects.get(object_name) if not obj: return {"error": f"Object not found: {object_name}"} - + # Make sure object can accept materials if not hasattr(obj, 'data') or not hasattr(obj.data, 'materials'): return {"error": f"Object {object_name} cannot accept materials"} - + # Find all images related to this texture and ensure they're properly loaded texture_images = {} for img in bpy.data.images: if img.name.startswith(texture_id + "_"): # Extract the map type from the image name map_type = img.name.split('_')[-1].split('.')[0] - + # Force a reload of the image img.reload() - + # Ensure proper color space if map_type.lower() in ['color', 'diffuse', 'albedo']: try: @@ -997,14 +1084,14 @@ def set_texture(self, object_name, texture_id): img.colorspace_settings.name = 'Non-Color' except: pass - + # Ensure the image is packed if not img.packed_file: img.pack() - + texture_images[map_type] = img print(f"Loaded texture map: {map_type} - {img.name}") - + # Debug info print(f"Image size: {img.size[0]}x{img.size[1]}") print(f"Color space: {img.colorspace_settings.name}") @@ -1013,53 +1100,53 @@ def set_texture(self, object_name, texture_id): if not texture_images: return {"error": f"No texture images found for: {texture_id}. Please download the texture first."} - + # Create a new material new_mat_name = f"{texture_id}_material_{object_name}" - + # Remove any existing material with this name to avoid conflicts existing_mat = bpy.data.materials.get(new_mat_name) if existing_mat: bpy.data.materials.remove(existing_mat) - + new_mat = bpy.data.materials.new(name=new_mat_name) new_mat.use_nodes = True - + # Set up the material nodes nodes = new_mat.node_tree.nodes links = new_mat.node_tree.links - + # Clear default nodes nodes.clear() - + # Create output node output = nodes.new(type='ShaderNodeOutputMaterial') output.location = (600, 0) - + # Create principled BSDF node principled = nodes.new(type='ShaderNodeBsdfPrincipled') principled.location = (300, 0) links.new(principled.outputs[0], output.inputs[0]) - + # Add texture nodes based on available maps tex_coord = nodes.new(type='ShaderNodeTexCoord') tex_coord.location = (-800, 0) - + mapping = nodes.new(type='ShaderNodeMapping') mapping.location = (-600, 0) mapping.vector_type = 'TEXTURE' # Changed from default 'POINT' to 'TEXTURE' links.new(tex_coord.outputs['UV'], mapping.inputs['Vector']) - + # Position offset for texture nodes x_pos = -400 y_pos = 300 - + # Connect different texture maps for map_type, image in texture_images.items(): tex_node = nodes.new(type='ShaderNodeTexImage') tex_node.location = (x_pos, y_pos) tex_node.image = image - + # Set color space based on map type if map_type.lower() in ['color', 'diffuse', 'albedo']: try: @@ -1071,9 +1158,9 @@ def set_texture(self, object_name, texture_id): tex_node.image.colorspace_settings.name = 'Non-Color' except: pass # Use default if Non-Color not available - + links.new(mapping.outputs['Vector'], tex_node.inputs['Vector']) - + # Connect to appropriate input on Principled BSDF if map_type.lower() in ['color', 'diffuse', 'albedo']: links.new(tex_node.outputs['Color'], principled.inputs['Base Color']) @@ -1094,12 +1181,12 @@ def set_texture(self, object_name, texture_id): disp_node.inputs['Scale'].default_value = 0.1 # Reduce displacement strength links.new(tex_node.outputs['Color'], disp_node.inputs['Height']) links.new(disp_node.outputs['Displacement'], output.inputs['Displacement']) - + y_pos -= 250 - + # Second pass: Connect nodes with proper handling for special cases texture_nodes = {} - + # First find all texture nodes and store them by map type for node in nodes: if node.type == 'TEX_IMAGE' and node.image: @@ -1107,7 +1194,7 @@ def set_texture(self, object_name, texture_id): if node.image == image: texture_nodes[map_type] = node break - + # Now connect everything using the nodes instead of images # Handle base color (diffuse) for map_name in ['color', 'diffuse', 'albedo']: @@ -1115,21 +1202,21 @@ def set_texture(self, object_name, texture_id): links.new(texture_nodes[map_name].outputs['Color'], principled.inputs['Base Color']) print(f"Connected {map_name} to Base Color") break - + # Handle roughness for map_name in ['roughness', 'rough']: if map_name in texture_nodes: links.new(texture_nodes[map_name].outputs['Color'], principled.inputs['Roughness']) print(f"Connected {map_name} to Roughness") break - + # Handle metallic for map_name in ['metallic', 'metalness', 'metal']: if map_name in texture_nodes: links.new(texture_nodes[map_name].outputs['Color'], principled.inputs['Metallic']) print(f"Connected {map_name} to Metallic") break - + # Handle normal maps for map_name in ['gl', 'dx', 'nor']: if map_name in texture_nodes: @@ -1139,7 +1226,7 @@ def set_texture(self, object_name, texture_id): links.new(normal_map_node.outputs['Normal'], principled.inputs['Normal']) print(f"Connected {map_name} to Normal") break - + # Handle displacement for map_name in ['displacement', 'disp', 'height']: if map_name in texture_nodes: @@ -1150,47 +1237,47 @@ def set_texture(self, object_name, texture_id): links.new(disp_node.outputs['Displacement'], output.inputs['Displacement']) print(f"Connected {map_name} to Displacement") break - + # Handle ARM texture (Ambient Occlusion, Roughness, Metallic) if 'arm' in texture_nodes: separate_rgb = nodes.new(type='ShaderNodeSeparateRGB') separate_rgb.location = (-200, -100) links.new(texture_nodes['arm'].outputs['Color'], separate_rgb.inputs['Image']) - + # Connect Roughness (G) if no dedicated roughness map if not any(map_name in texture_nodes for map_name in ['roughness', 'rough']): links.new(separate_rgb.outputs['G'], principled.inputs['Roughness']) print("Connected ARM.G to Roughness") - + # Connect Metallic (B) if no dedicated metallic map if not any(map_name in texture_nodes for map_name in ['metallic', 'metalness', 'metal']): links.new(separate_rgb.outputs['B'], principled.inputs['Metallic']) print("Connected ARM.B to Metallic") - + # For AO (R channel), multiply with base color if we have one base_color_node = None for map_name in ['color', 'diffuse', 'albedo']: if map_name in texture_nodes: base_color_node = texture_nodes[map_name] break - + if base_color_node: mix_node = nodes.new(type='ShaderNodeMixRGB') mix_node.location = (100, 200) mix_node.blend_type = 'MULTIPLY' mix_node.inputs['Fac'].default_value = 0.8 # 80% influence - + # Disconnect direct connection to base color for link in base_color_node.outputs['Color'].links: if link.to_socket == principled.inputs['Base Color']: links.remove(link) - + # Connect through the mix node links.new(base_color_node.outputs['Color'], mix_node.inputs[1]) links.new(separate_rgb.outputs['R'], mix_node.inputs[2]) links.new(mix_node.outputs['Color'], principled.inputs['Base Color']) print("Connected ARM.R to AO mix with Base Color") - + # Handle AO (Ambient Occlusion) if separate if 'ao' in texture_nodes: base_color_node = None @@ -1198,41 +1285,41 @@ def set_texture(self, object_name, texture_id): if map_name in texture_nodes: base_color_node = texture_nodes[map_name] break - + if base_color_node: mix_node = nodes.new(type='ShaderNodeMixRGB') mix_node.location = (100, 200) mix_node.blend_type = 'MULTIPLY' mix_node.inputs['Fac'].default_value = 0.8 # 80% influence - + # Disconnect direct connection to base color for link in base_color_node.outputs['Color'].links: if link.to_socket == principled.inputs['Base Color']: links.remove(link) - + # Connect through the mix node links.new(base_color_node.outputs['Color'], mix_node.inputs[1]) links.new(texture_nodes['ao'].outputs['Color'], mix_node.inputs[2]) links.new(mix_node.outputs['Color'], principled.inputs['Base Color']) print("Connected AO to mix with Base Color") - + # CRITICAL: Make sure to clear all existing materials from the object while len(obj.data.materials) > 0: obj.data.materials.pop(index=0) - + # Assign the new material to the object obj.data.materials.append(new_mat) - + # CRITICAL: Make the object active and select it bpy.context.view_layer.objects.active = obj obj.select_set(True) - + # CRITICAL: Force Blender to update the material bpy.context.view_layer.update() - + # Get the list of texture maps texture_maps = list(texture_images.keys()) - + # Get info about texture nodes for debugging material_info = { "name": new_mat.name, @@ -1240,21 +1327,21 @@ def set_texture(self, object_name, texture_id): "node_count": len(new_mat.node_tree.nodes), "texture_nodes": [] } - + for node in new_mat.node_tree.nodes: if node.type == 'TEX_IMAGE' and node.image: connections = [] for output in node.outputs: for link in output.links: connections.append(f"{output.name} → {link.to_node.name}.{link.to_socket.name}") - + material_info["texture_nodes"].append({ "name": node.name, "image": node.image.name, "colorspace": node.image.colorspace_settings.name, "connections": connections }) - + return { "success": True, "message": f"Created new material and applied texture {texture_id} to {object_name}", @@ -1262,7 +1349,7 @@ def set_texture(self, object_name, texture_id): "maps": texture_maps, "material_info": material_info } - + except Exception as e: print(f"Error in set_texture: {str(e)}") traceback.print_exc() @@ -1275,21 +1362,21 @@ def get_polyhaven_status(self): return {"enabled": True, "message": "PolyHaven integration is enabled and ready to use."} else: return { - "enabled": False, + "enabled": False, "message": """PolyHaven integration is currently disabled. To enable it: 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) 2. Check the 'Use assets from Poly Haven' checkbox 3. Restart the connection to Claude""" - } + } - #region Hyper3D + # region Hyper3D def get_hyper3d_status(self): """Get the current status of Hyper3D Rodin integration""" enabled = bpy.context.scene.blendermcp_use_hyper3d if enabled: if not bpy.context.scene.blendermcp_hyper3d_api_key: return { - "enabled": False, + "enabled": False, "message": """Hyper3D Rodin integration is currently enabled, but API key is not given. To enable it: 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) 2. Keep the 'Use Hyper3D Rodin 3D model generation' checkbox checked @@ -1298,14 +1385,14 @@ def get_hyper3d_status(self): } mode = bpy.context.scene.blendermcp_hyper3d_mode message = f"Hyper3D Rodin integration is enabled and ready to use. Mode: {mode}. " + \ - f"Key type: {'private' if bpy.context.scene.blendermcp_hyper3d_api_key != RODIN_FREE_TRIAL_KEY else 'free_trial'}" + f"Key type: {'private' if bpy.context.scene.blendermcp_hyper3d_api_key != RODIN_FREE_TRIAL_KEY else 'free_trial'}" return { "enabled": True, "message": message } else: return { - "enabled": False, + "enabled": False, "message": """Hyper3D Rodin integration is currently disabled. To enable it: 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) 2. Check the 'Use Hyper3D Rodin 3D model generation' checkbox @@ -1323,10 +1410,10 @@ def create_rodin_job(self, *args, **kwargs): def create_rodin_job_main_site( self, - text_prompt: str=None, - images: list[tuple[str, str]]=None, + text_prompt: str = None, + images: list[tuple[str, str]] = None, bbox_condition=None - ): + ): try: if images is None: images = [] @@ -1351,13 +1438,13 @@ def create_rodin_job_main_site( return data except Exception as e: return {"error": str(e)} - + def create_rodin_job_fal_ai( self, - text_prompt: str=None, - images: list[tuple[str, str]]=None, + text_prompt: str = None, + images: list[tuple[str, str]] = None, bbox_condition=None - ): + ): try: req_data = { "tier": "Sketch", @@ -1405,7 +1492,7 @@ def poll_rodin_job_status_main_site(self, subscription_key: str): return { "status_list": [i["status"] for i in data["jobs"]] } - + def poll_rodin_job_status_fal_ai(self, request_id: str): """Call the job status API to get the job status""" response = requests.get( @@ -1424,21 +1511,21 @@ def _clean_imported_glb(filepath, mesh_name=None): # Import the GLB file bpy.ops.import_scene.gltf(filepath=filepath) - + # Ensure the context is updated bpy.context.view_layer.update() - + # Get all imported objects imported_objects = list(set(bpy.data.objects) - existing_objects) # imported_objects = [obj for obj in bpy.context.view_layer.objects if obj.select_get()] - + if not imported_objects: print("Error: No objects were imported.") return - + # Identify the mesh object mesh_obj = None - + if len(imported_objects) == 1 and imported_objects[0].type == 'MESH': mesh_obj = imported_objects[0] print("Single mesh imported, no cleanup needed.") @@ -1448,14 +1535,14 @@ def _clean_imported_glb(filepath, mesh_name=None): potential_mesh = parent_obj.children[0] if potential_mesh.type == 'MESH': print("GLB structure confirmed: Empty node with one mesh child.") - + # Unparent the mesh from the empty node potential_mesh.parent = None - + # Remove the empty node bpy.data.objects.remove(parent_obj) print("Removed empty node, keeping only the mesh.") - + mesh_obj = potential_mesh else: print("Error: Child is not a mesh object.") @@ -1463,7 +1550,7 @@ def _clean_imported_glb(filepath, mesh_name=None): else: print("Error: Expected an empty node with one mesh child or a single mesh object.") return - + # Rename the mesh if needed try: if mesh_obj and mesh_obj.name is not None and mesh_name: @@ -1505,27 +1592,27 @@ def import_generated_asset_main_site(self, task_uuid: str, name: str): prefix=task_uuid, suffix=".glb", ) - + try: # Download the content response = requests.get(i["url"], stream=True) response.raise_for_status() # Raise an exception for HTTP errors - + # Write the content to the temporary file for chunk in response.iter_content(chunk_size=8192): temp_file.write(chunk) - + # Close the file temp_file.close() - + except Exception as e: # Clean up the file if there's an error temp_file.close() os.unlink(temp_file.name) return {"succeed": False, "error": str(e)} - + break - + try: obj = self._clean_imported_glb( filepath=temp_file.name, @@ -1542,13 +1629,13 @@ def import_generated_asset_main_site(self, task_uuid: str, name: str): if obj.type == "MESH": bounding_box = self._get_aabb(obj) result["world_bounding_box"] = bounding_box - + return { "succeed": True, **result } except Exception as e: return {"succeed": False, "error": str(e)} - + def import_generated_asset_fal_ai(self, request_id: str, name: str): """Fetch the generated asset, import into blender""" response = requests.get( @@ -1559,7 +1646,7 @@ def import_generated_asset_fal_ai(self, request_id: str, name: str): ) data_ = response.json() temp_file = None - + temp_file = tempfile.NamedTemporaryFile( delete=False, prefix=request_id, @@ -1570,14 +1657,14 @@ def import_generated_asset_fal_ai(self, request_id: str, name: str): # Download the content response = requests.get(data_["model_mesh"]["url"], stream=True) response.raise_for_status() # Raise an exception for HTTP errors - + # Write the content to the temporary file for chunk in response.iter_content(chunk_size=8192): temp_file.write(chunk) - + # Close the file temp_file.close() - + except Exception as e: # Clean up the file if there's an error temp_file.close() @@ -1600,13 +1687,940 @@ def import_generated_asset_fal_ai(self, request_id: str, name: str): if obj.type == "MESH": bounding_box = self._get_aabb(obj) result["world_bounding_box"] = bounding_box - + return { "succeed": True, **result } except Exception as e: return {"succeed": False, "error": str(e)} - #endregion + + # endregion + + # region GeometryNodeCreator + def complete_geometry_node(self, object_name, nodes, links, input_sockets=None): + """Complete geometry node network creation + + Args: + object_name: Object name + nodes: List of node definitions + links: List of node connections + input_sockets: Node group input interface definitions [{"name": "Name", "type": "Type", "value": "DefaultValue"}] + + Returns: + dict: Dictionary containing operation status and related information + """ + try: + obj = bpy.data.objects.get(object_name) + if not obj: + result = self._create_geometry_nodes_object(object_name) + if "error" in result: + return result + obj = bpy.data.objects.get(object_name) + + # Find geometry nodes modifier + geometry_modifier = None + for modifier in obj.modifiers: + if modifier.type == 'NODES': + geometry_modifier = modifier + break + + if geometry_modifier and geometry_modifier.node_group: + old_node_group_name = geometry_modifier.node_group.name + geometry_modifier.node_group = None + + # Try to delete the old node group + old_node_group = bpy.data.node_groups.get(old_node_group_name) + if old_node_group: + bpy.data.node_groups.remove(old_node_group) + + # If there's no geometry nodes modifier, create one + if not geometry_modifier: + geometry_modifier = obj.modifiers.new(name="GeometryNodes", type='NODES') + + # Create a new node group + node_group = bpy.data.node_groups.new(name=f"{object_name}_geometry", type='GeometryNodeTree') + if IS_BLENDER_4: + node_group.is_modifier = True + + # Set the node group for the modifier + geometry_modifier.node_group = node_group + + has_input_node = False + has_output_node = False + for node_data in nodes: + node_type = node_data.get("type", "") + if node_type in ['NodeGroupInput', 'GroupInput']: + has_input_node = True + elif node_type in ['NodeGroupOutput', 'GroupOutput']: + has_output_node = True + + self._setup_node_group_interface(node_group, input_sockets) + + # First create all nodes + created_nodes = [] + + input_node = None + output_node = None + + if not has_input_node: + input_node = node_group.nodes.new('NodeGroupInput') + input_node.location = (-300, 0) + + if not has_output_node: + output_node = node_group.nodes.new('NodeGroupOutput') + output_node.location = (500, 0) + + for node_data in nodes: + try: + node_type = node_data["type"] + + node = node_group.nodes.new(node_type) + + if "location" in node_data: + node.location = node_data["location"] + + if "label" in node_data: + node.label = node_data["label"] + + if "properties" in node_data: + for prop_name, prop_value in node_data["properties"].items(): + if hasattr(node, prop_name): + try: + setattr(node, prop_name, prop_value) + except Exception as prop_error: + print(f"Error setting property {prop_name}: {prop_error}") + + if "inputs" in node_data: + for input_name, input_value in node_data["inputs"].items(): + for input in node.inputs: + if input.name == input_name and hasattr(input, 'default_value'): + try: + if hasattr(input.default_value, '__len__'): + for i, val in enumerate(input_value): + if i < len(input.default_value): + input.default_value[i] = val + else: + input.default_value = input_value + except Exception as input_error: + print(f"Error setting input value: {input_error}") + + node_info = { + "name": node.name, + "type": node.type, + "location": [node.location.x, node.location.y] + } + + created_nodes.append(node_info) + except Exception as node_error: + return {"error": f"Error creating node {node_data['type']}: {str(node_error)}"} + + # Create links + created_links = [] + + for link_data in links: + try: + # Find nodes by index or name + from_node_id = link_data["from_node"] + to_node_id = link_data["to_node"] + + from_node = None + to_node = None + + # Special handling for "input" and "output" identifiers + if from_node_id == "input": + from_node = input_node + elif from_node_id == "output": + from_node = output_node + elif isinstance(from_node_id, int): + # Assume integer index points to created node + if 0 <= from_node_id < len(created_nodes): + from_node = node_group.nodes.get(created_nodes[from_node_id]["name"]) + else: + # Find by name + from_node = node_group.nodes.get(from_node_id) + + if to_node_id == "input": + to_node = input_node + elif to_node_id == "output": + to_node = output_node + elif isinstance(to_node_id, int): + # Assume integer index points to created node + if 0 <= to_node_id < len(created_nodes): + to_node = node_group.nodes.get(created_nodes[to_node_id]["name"]) + else: + # Find by name + to_node = node_group.nodes.get(to_node_id) + + if not from_node: + return {"error": f"Could not find source node: {from_node_id}"} + + if not to_node: + return {"error": f"Could not find target node: {to_node_id}"} + + # Find socket + from_socket_id = link_data["from_socket"] + to_socket_id = link_data["to_socket"] + + from_socket = None + to_socket = None + + # Find socket by index or name + if isinstance(from_socket_id, int): + if 0 <= from_socket_id < len(from_node.outputs): + from_socket = from_node.outputs[from_socket_id] + else: + # Search by name + for socket in from_node.outputs: + if socket.name == from_socket_id: + from_socket = socket + break + + if isinstance(to_socket_id, int): + if 0 <= to_socket_id < len(to_node.inputs): + to_socket = to_node.inputs[to_socket_id] + else: + # Search by name + for socket in to_node.inputs: + if socket.name == to_socket_id: + to_socket = socket + break + + if not from_socket: + return {"error": f"Could not find source socket: {from_socket_id}"} + + if not to_socket: + return {"error": f"Could not find target socket: {to_socket_id}"} + + # Create connection + link = node_group.links.new(from_socket, to_socket) + + # Record created link + link_info = { + "from_node": from_node.name, + "from_socket": from_socket.name, + "to_node": to_node.name, + "to_socket": to_socket.name + } + + created_links.append(link_info) + except Exception as link_error: + return {"error": f"Error creating link: {str(link_error)}"} + + # If input sockets are provided and contain values, set modifier parameters + if input_sockets: + try: + # Find the NodeGroupInput node + group_input_node = None + for node in node_group.nodes: + if node.type == 'GROUP_INPUT': + group_input_node = node + break + + # Create one if not found + if not group_input_node: + group_input_node = node_group.nodes.new('NodeGroupInput') + + # Get socket information directly from the NodeGroupInput node + socket_dict = {} + for i, output in enumerate(group_input_node.outputs): + socket_dict[output.name] = i + + # Output debug information + print(f"Sockets from NodeGroupInput: {socket_dict}") + + # Set modifier parameter values + for socket in input_sockets: + if "value" in socket and socket.get("type") != "NodeSocketGeometry": + socket_name = socket.get("name") + + if socket_name in socket_dict: + socket_index = socket_dict[socket_name] + socket_key = f"Socket_{socket_index}" + + try: + if socket_key in geometry_modifier: + geometry_modifier[socket_key] = socket["value"] + print( + f"Successfully set parameter {socket_name} ({socket_key}) = {socket['value']}") + else: + print(f"Warning: {socket_key} does not exist in modifier") + except Exception as e: + print(f"Error when setting modifier parameter {socket_key}: {str(e)}") + else: + print(f"Warning: Could not find socket named '{socket_name}' in NodeGroupInput") + + # Output all available Socket keys for debugging + available_sockets = [key for key in geometry_modifier.keys() if key.startswith("Socket_")] + if available_sockets: + print(f"Available Socket keys in modifier: {available_sockets}") + + except Exception as e: + print(f"Error setting modifier parameters: {str(e)}") + # Fall back to compatibility mode + try: + # Directly iterate through modifier properties and set values + for socket in input_sockets: + if "value" in socket and socket.get("type") != "NodeSocketGeometry": + socket_name = socket.get("name") + # Try to find matching socket keys + for key in geometry_modifier.keys(): + if key.startswith("Socket_"): + # Try to set directly + try: + geometry_modifier[key] = socket["value"] + print(f"Directly set {key} = {socket['value']}") + break + except: + pass + except: + pass + + return { + "status": "success", + "object_name": object_name, + "modifier_name": geometry_modifier.name, + "node_group_name": node_group.name, + "nodes": created_nodes, + "links": created_links + } + except Exception as e: + return {"error": f"Error completing geometry node network: {str(e)}"} + + def _create_geometry_nodes_object(self, object_name): + """Create a base object and add a geometry nodes modifier + + Args: + object_name: Object name + + Returns: + dict: Dictionary containing operation status and related information + """ + try: + # Create base object + bpy.ops.mesh.primitive_cube_add() + obj = bpy.context.active_object + obj.name = object_name + + # Create node group + node_group = bpy.data.node_groups.new(name=f"{object_name}_geometry", type='GeometryNodeTree') + if IS_BLENDER_4: + node_group.is_modifier = True + + # Create interface + if IS_BLENDER_4: + interface = node_group.interface + interface.new_socket(name="Geometry", in_out='INPUT', socket_type='NodeSocketGeometry') + interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry') + else: + node_group.inputs.new("NodeSocketGeometry", "Geometry") + node_group.outputs.new("NodeSocketGeometry", "Geometry") + + # Add input and output nodes + group_in = node_group.nodes.new('NodeGroupInput') + group_in.location = (-200, 0) + + group_out = node_group.nodes.new('NodeGroupOutput') + group_out.location = (200, 0) + + # Connect geometry flow + node_group.links.new(group_in.outputs["Geometry"], group_out.inputs["Geometry"]) + + # Create geometry nodes modifier and immediately set the node group + mod = obj.modifiers.new(name="GeometryNodes", type='NODES') + + # Ensure the node group exists + if node_group.name not in bpy.data.node_groups: + return {"error": "Failed to create node group"} + + # Set the modifier's node group + mod.node_group = node_group + + # Verify the setup was successful + if not mod.node_group: + return {"error": "Failed to set modifier node group"} + + return { + "status": "success", + "object_name": obj.name, + "modifier_name": mod.name, + "node_group_name": node_group.name + } + except Exception as e: + return {"error": f"Error creating geometry nodes object: {str(e)}"} + + def _setup_node_group_interface(self, node_group, input_sockets=None, output_sockets=None): + """Set up input and output interfaces for the node group + + Args: + node_group: Node group + input_sockets: Input interface list [{"name": "Name", "type": "Type"}] + output_sockets: Output interface list [{"name": "Name", "type": "Type"}] + """ + # Default interfaces + default_input_sockets = [{"name": "Geometry", "type": "NodeSocketGeometry"}] + default_output_sockets = [{"name": "Geometry", "type": "NodeSocketGeometry"}] + + # Use provided interfaces or default values + input_sockets = input_sockets or default_input_sockets + output_sockets = output_sockets or default_output_sockets + + # Clear existing interfaces + if IS_BLENDER_4: + # Blender 4.0+ + interface = node_group.interface + + # Clear existing interfaces + sockets_to_remove = [] + for socket in interface.items_tree: + sockets_to_remove.append(socket) + + for socket in sockets_to_remove: + interface.remove(socket) + + # Create new interfaces + for socket in input_sockets: + interface.new_socket( + name=socket["name"], + in_out='INPUT', + socket_type=socket["type"] + ) + + for socket in output_sockets: + interface.new_socket( + name=socket["name"], + in_out='OUTPUT', + socket_type=socket["type"] + ) + else: + # Blender 3.x + # Clear existing inputs + for i in range(len(node_group.inputs) - 1, -1, -1): + node_group.inputs.remove(node_group.inputs[i]) + + # Clear existing outputs + for i in range(len(node_group.outputs) - 1, -1, -1): + node_group.outputs.remove(node_group.outputs[i]) + + # Create new interfaces + for socket in input_sockets: + node_group.inputs.new(socket["type"], socket["name"]) + + for socket in output_sockets: + node_group.outputs.new(socket["type"], socket["name"]) + + # endregion + + # region GeometryNodeInfo + def get_node_info(self, output_format='text', include_details=False, node_type_name=None): + """Get node type information + + Args: + output_format: Output format ('text', 'json') + include_details: Whether to include detailed information (properties and sockets) + node_type_name: Node type name, can be a single string, comma-separated multiple node names, or a list of strings + + Returns: + str: Returns node information in different formats based on output_format: + - 'text': String, each line as "TypeName:Description" and detailed information (if include_details is True) + - 'json': JSON formatted string + """ + # Handle include_details type conversion + try: + if isinstance(include_details, str): + include_details_lower = include_details.lower() + include_details = include_details_lower == 'true' or include_details_lower == 'yes' or include_details_lower == '1' + + # Handle node_type_name parameter, ensuring it's a list + node_type_names = [] + if node_type_name is not None: + # If it's a string, check if it's a comma-separated list + if isinstance(node_type_name, str): + # Split by comma and strip whitespace + node_type_names = [name.strip() for name in node_type_name.split(',') if name.strip()] + # If it's already a list, use it directly + elif isinstance(node_type_name, list): + node_type_names = node_type_name + else: + raise ValueError(f"node_type_name must be a string or a list, received: {type(node_type_name)}") + + # Ensure all elements in the list are strings + if not all(isinstance(item, str) for item in node_type_names): + raise ValueError(f"All elements in node_type_name list must be strings") + + print(f"Processing node type names: {node_type_names} (from: {node_type_name})") + + # Handle output_format parameter + if output_format not in ('text', 'json'): + raise ValueError(f"Unsupported output format: {output_format}, please use 'text' or 'json'") + + self._register_node_info_cache() + + # Get all node information + node_infos = self._get_nodes_from_cache_or_collect() + + # If specific node type names are specified, return only those nodes' information + if node_type_names: + # Find target nodes + target_nodes = [node for node in node_infos if node.name in node_type_names] + + # If no target nodes are found + if not target_nodes: + if len(node_type_names) == 1: + return f"Node {node_type_names[0]} does not exist or cannot be created" + else: + return f"Specified nodes do not exist or cannot be created: {', '.join(node_type_names)}" + + # Return node information based on output_format + if output_format == 'text': + if len(target_nodes) == 1: + return self._format_single_node_text(target_nodes[0], include_details) + else: + if include_details: + return "\n\n".join(self._format_node_text(node, True) for node in target_nodes) + else: + return '\n'.join(self._format_node_text(node, False) for node in target_nodes) + else: # 'json' + node_dicts = [self._node_to_dict(node, include_details) for node in target_nodes] + if len(node_dicts) == 1: + return json.dumps(node_dicts[0], ensure_ascii=False, indent=2, default=str) + else: + return json.dumps(node_dicts, ensure_ascii=False, indent=2, default=str) + + # Return information about all nodes based on output_format + if output_format == 'text': + if include_details: + return "\n\n".join(self._format_node_text(node, True) for node in node_infos) + else: + return '\n'.join(self._format_node_text(node, False) for node in node_infos) + + else: # 'json' + node_dicts = [self._node_to_dict(node, include_details) for node in node_infos] + return json.dumps(node_dicts, ensure_ascii=False, indent=2, default=str) + + except Exception as e: + print(f"Error getting node information: {str(e)}") + import traceback + traceback.print_exc() + return f"Error getting node information: {str(e)}" + + def _register_node_info_cache(self): + """Register custom properties""" + if not hasattr(bpy.types.WindowManager, "node_info_cache"): + bpy.types.WindowManager.node_info_cache = bpy.props.StringProperty( + name="Node Info Cache", + description="Cache for node information", + default="" + ) + + def _collect_socket_info(self, node, is_input: bool = True) -> List[SocketInfo]: + """Collect node socket information + + Args: + node: Node object + is_input: Whether the socket is an input socket + + Returns: + List[SocketInfo]: List of socket information + """ + sockets = [] + socket_list = node.inputs if is_input else node.outputs + + for socket in socket_list: + # Default value handling + default_value = None + if hasattr(socket, 'default_value'): + try: + # Handling different types of default values + if hasattr(socket.default_value, '__len__'): + # Vectors, colors, etc. + default_value = list(socket.default_value) + else: + # Scalar values + default_value = socket.default_value + except: + default_value = None + + # Create SocketInfo object + socket_info = SocketInfo( + name=socket.name, + type=socket.type, + description=socket.description, + identifier='', + enabled=socket.enabled, + hide=socket.hide, + default_value=default_value + ) + + sockets.append(socket_info) + + return sockets + + def _collect_property_info(self, node_type) -> List[PropertyInfo]: + """Collect node property information + + Args: + node_type: Node type + + Returns: + List[PropertyInfo]: List of property information + """ + properties = [] + + # Get parent class identifier, to exclude inherited properties + parent_props = [prop.identifier for base in node_type.__bases__ + for prop in base.bl_rna.properties] + + # Iterate over node type's properties + for prop in node_type.bl_rna.properties: + # Skip inherited properties and built-in basic properties + if (prop.identifier in parent_props or + prop.identifier in ['rna_type', 'name', 'location', 'width', + 'width_hidden', 'height', 'dimensions', + 'inputs', 'outputs', 'internal_links']): + continue + + # Get default value (based on property type) + default_value = None + enum_items = [] + + # Handling different types of properties + if prop.type == 'ENUM': + # Handling enum types + enum_items = [{'identifier': item.identifier, + 'name': item.name, + 'description': item.description} + for item in prop.enum_items] + elif hasattr(prop, 'default'): + default_value = prop.default + + # Create PropertyInfo object + prop_info = PropertyInfo( + identifier=prop.identifier, + name=prop.name, + description=prop.description, + type=prop.type, + default_value=default_value, + enum_items=enum_items + ) + + properties.append(prop_info) + + return properties + + def _verify_node_identifier(self, node_type_name: str) -> Tuple[ + bool, List[SocketInfo], List[SocketInfo], List[PropertyInfo]]: + """Verify node identifier is valid for creating a node and get node information + + Args: + node_type_name: Node type name + + Returns: + Tuple: (is_usable, input_sockets, output_sockets, property_info) + """ + # Create a temporary geometry node tree for testing + temp_tree = bpy.data.node_groups.new('TempNodeTree', 'GeometryNodeTree') + inputs = [] + outputs = [] + properties = [] + + try: + # Try to create a node + node = temp_tree.nodes.new(node_type_name) + node_type = getattr(bpy.types, node_type_name) + + # Get socket information + inputs = self._collect_socket_info(node, is_input=True) + outputs = self._collect_socket_info(node, is_input=False) + + # Get property information + properties = self._collect_property_info(node_type) + + return True, inputs, outputs, properties + except Exception as e: + return False, inputs, outputs, properties + finally: + # Clean up + bpy.data.node_groups.remove(temp_tree) + + def _collect_node_info(self) -> List[NodeInfo]: + """Collect node information (internal function)""" + # Exclude list + denylist = {'filter'} + class_denylist = {'CompositorNodeMath', 'TextureNodeMath'} + + # Collect node information + node_infos = [] + + for node_type_name in dir(bpy.types): + # Get type object + node_type = getattr(bpy.types, node_type_name) + + # Check if it's a node type + if (isinstance(node_type, type) and + issubclass(node_type, bpy.types.Node) and + node_type.is_registered_node_type() and + node_type.bl_rna.name.lower() not in denylist and + node_type.__name__ not in class_denylist): + + # Verify if the node can be created in the geometry node tree and get information + valid, inputs, outputs, properties = self._verify_node_identifier(node_type.__name__) + if not valid: + continue + + # Get description + description = node_type.bl_rna.description + + # Get identifier for creating the node + node_identifier = node_type.__name__ + + # Create NodeInfo object + node_info = NodeInfo( + name=node_identifier, + description=description or "No description available", + inputs=inputs, + outputs=outputs, + properties=properties + ) + + node_infos.append(node_info) + + # Sort by name + node_infos.sort(key=lambda x: x.name) + return node_infos + + def _get_nodes_from_cache_or_collect(self) -> List[NodeInfo]: + """Get node information from cache or collect new ones""" + wm = bpy.context.window_manager + + # Try to get data from cache + if wm.node_info_cache: + print(f"Using cache..") + try: + # Deserialize from cache + cache_data = json.loads(wm.node_info_cache) + node_infos = [] + + # Rebuild NodeInfo objects + for item in cache_data: + # Rebuild input sockets + inputs = [SocketInfo(**s) for s in item.get('inputs', [])] + # Rebuild output sockets + outputs = [SocketInfo(**s) for s in item.get('outputs', [])] + # Rebuild property information + properties = [] + for p in item.get('properties', []): + prop = PropertyInfo( + identifier=p['identifier'], + name=p['name'], + description=p['description'], + type=p['type'], + default_value=p.get('default_value'), + enum_items=p.get('enum_items', []) + ) + properties.append(prop) + + # Create NodeInfo object + node_infos.append(NodeInfo( + name=item['name'], + description=item['description'], + inputs=inputs, + outputs=outputs, + properties=properties + )) + return node_infos + except Exception as e: + print(f"Error parsing cache: {e}") + + # Collect node information + print(f"Collecting node information..") + node_infos = self._collect_node_info() + + # Update cache + self._update_cache(node_infos) + return node_infos + + def _format_socket_info_text(self, socket: SocketInfo, index: int, indent: str = " ") -> List[str]: + """Format socket information as a list of text lines""" + lines = [f"{indent}{index}. {socket.name} ({socket.type})"] + if socket.description: + lines.append(f"{indent} Description: {socket.description}") + if socket.default_value is not None: + lines.append(f"{indent} Default value: {socket.default_value}") + return lines + + def _format_property_info_text(self, prop: PropertyInfo, index: int, indent: str = " ") -> List[str]: + """Format property information as a list of text lines""" + lines = [f"{indent}{index}. {prop.name} ({prop.type})"] + if prop.description: + lines.append(f"{indent} Description: {prop.description}") + + if prop.default_value is not None: + lines.append(f"{indent} Default value: {prop.default_value}") + + if prop.enum_items: + lines.append(f"{indent} Options:") + for i, item in enumerate(prop.enum_items): + lines.append(f"{indent} {i}. {item['name']} ('{item['identifier']}')") + if item['description']: + lines.append(f"{indent} Description: {item['description']}") + + return lines + + def _format_node_text(self, node: NodeInfo, include_details: bool = False) -> str: + """Format node information as text""" + if not include_details: + return f"{node.name}:{node.description}" + + lines = [f"{node.name}:{node.description}"] + + # Add property information + if node.properties: + lines.append(" Properties:") + for i, prop in enumerate(node.properties): + lines.extend(self._format_property_info_text(prop, i, " ")) + + # Add input socket information + if node.inputs: + lines.append(" Input sockets:") + for i, socket in enumerate(node.inputs): + lines.extend(self._format_socket_info_text(socket, i, " ")) + + # Add output socket information + if node.outputs: + lines.append(" Output sockets:") + for i, socket in enumerate(node.outputs): + lines.extend(self._format_socket_info_text(socket, i, " ")) + + return "\n".join(lines) + + def _format_single_node_text(self, node: NodeInfo, include_details: bool = False) -> str: + """Format single node information as detailed text""" + lines = [f"Node: {node.name}", f"Description: {node.description}"] + + if include_details: + # Add property information + if node.properties: + lines.append("\nProperties:") + for i, prop in enumerate(node.properties): + lines.append(f" {i}. {prop.name} ({prop.type})") + if prop.description: + lines.append(f" Description: {prop.description}") + if prop.default_value is not None: + lines.append(f" Default value: {prop.default_value}") + if prop.enum_items: + lines.append(f" Options:") + for j, item in enumerate(prop.enum_items): + lines.append(f" {j}. {item['name']} ('{item['identifier']}')") + if item['description']: + lines.append(f" Description: {item['description']}") + + # Add input socket information + if node.inputs: + lines.append("\nInput sockets:") + for i, socket in enumerate(node.inputs): + lines.append(f" {i}. {socket.name} ({socket.type})") + if socket.description: + lines.append(f" Description: {socket.description}") + if socket.default_value is not None: + lines.append(f" Default value: {socket.default_value}") + + # Add output socket information + if node.outputs: + lines.append("\nOutput sockets:") + for i, socket in enumerate(node.outputs): + lines.append(f" {i}. {socket.name} ({socket.type})") + if socket.description: + lines.append(f" Description: {socket.description}") + + return '\n'.join(lines) + + def _node_to_dict(self, node: NodeInfo, include_details: bool = False) -> dict: + """Convert node information to dictionary""" + if include_details: + return { + 'name': node.name, + 'description': node.description, + 'properties': [{ + 'identifier': p.identifier, + 'name': p.name, + 'description': p.description, + 'type': p.type, + 'default_value': p.default_value, + 'enum_items': p.enum_items + } for p in node.properties], + 'inputs': [socket.__dict__ for socket in node.inputs], + 'outputs': [socket.__dict__ for socket in node.outputs] + } + else: + return { + 'name': node.name, + 'description': node.description + } + + def _update_cache(self, node_infos: List[NodeInfo]): + """Update node information cache + + Args: + node_infos: List of NodeInfo objects + """ + wm = bpy.context.window_manager + + # Convert NodeInfo objects to serializable dictionary + serializable_data = [] + for info in node_infos: + node_dict = { + 'name': info.name, + 'description': info.description, + 'properties': [{ + 'identifier': p.identifier, + 'name': p.name, + 'description': p.description, + 'type': p.type, + 'default_value': p.default_value, + 'enum_items': p.enum_items + } for p in info.properties], + 'inputs': [socket.__dict__ for socket in info.inputs], + 'outputs': [socket.__dict__ for socket in info.outputs] + } + serializable_data.append(node_dict) + + # Update cache + try: + wm.node_info_cache = json.dumps(serializable_data, default=str) + except Exception as e: + print(f"Error updating cache: {e}") + + # endregion + + def get_geometry_nodes_status(self): + """Get the status of the geometry nodes feature in Blender. + + Returns: + dict: A dictionary containing information about the geometry nodes feature status. + """ + try: + # Get the geometry nodes setting from Blender preferences + enabled = bpy.context.scene.blendermcp_use_geometry_nodes + if enabled: + return { + "status": "success", + "enabled": True, + "message": "Geometry nodes feature is enabled." + } + else: + return { + "status": "warning", + "enabled": False, + "message": "Geometry nodes feature is disabled. Enable it in the BlenderMCP panel to use geometry node commands." + } + except Exception as e: + # Handle errors when checking status + return { + "status": "error", + "enabled": False, + "message": f"Error checking geometry nodes status: {str(e)}" + } + # Blender UI Panel class BLENDERMCP_PT_Panel(bpy.types.Panel): @@ -1615,11 +2629,11 @@ class BLENDERMCP_PT_Panel(bpy.types.Panel): bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'BlenderMCP' - + def draw(self, context): layout = self.layout scene = context.scene - + layout.prop(scene, "blendermcp_port") layout.prop(scene, "blendermcp_use_polyhaven", text="Use assets from Poly Haven") @@ -1628,61 +2642,67 @@ def draw(self, context): layout.prop(scene, "blendermcp_hyper3d_mode", text="Rodin Mode") layout.prop(scene, "blendermcp_hyper3d_api_key", text="API Key") layout.operator("blendermcp.set_hyper3d_free_trial_api_key", text="Set Free Trial API Key") - + + layout.prop(scene, "blendermcp_use_geometry_nodes", text="Use Geometry Nodes for procedural modeling") + if not scene.blendermcp_server_running: layout.operator("blendermcp.start_server", text="Start MCP Server") else: layout.operator("blendermcp.stop_server", text="Stop MCP Server") layout.label(text=f"Running on port {scene.blendermcp_port}") + # Operator to set Hyper3D API Key class BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey(bpy.types.Operator): bl_idname = "blendermcp.set_hyper3d_free_trial_api_key" bl_label = "Set Free Trial API Key" - + def execute(self, context): context.scene.blendermcp_hyper3d_api_key = RODIN_FREE_TRIAL_KEY context.scene.blendermcp_hyper3d_mode = 'MAIN_SITE' self.report({'INFO'}, "API Key set successfully!") return {'FINISHED'} + # Operator to start the server class BLENDERMCP_OT_StartServer(bpy.types.Operator): bl_idname = "blendermcp.start_server" bl_label = "Connect to Claude" bl_description = "Start the BlenderMCP server to connect with Claude" - + def execute(self, context): scene = context.scene - + # Create a new server instance if not hasattr(bpy.types, "blendermcp_server") or not bpy.types.blendermcp_server: bpy.types.blendermcp_server = BlenderMCPServer(port=scene.blendermcp_port) - + # Start the server bpy.types.blendermcp_server.start() scene.blendermcp_server_running = True - + return {'FINISHED'} + # Operator to stop the server class BLENDERMCP_OT_StopServer(bpy.types.Operator): bl_idname = "blendermcp.stop_server" bl_label = "Stop the connection to Claude" bl_description = "Stop the connection to Claude" - + def execute(self, context): scene = context.scene - + # Stop the server if it exists if hasattr(bpy.types, "blendermcp_server") and bpy.types.blendermcp_server: bpy.types.blendermcp_server.stop() del bpy.types.blendermcp_server - + scene.blendermcp_server_running = False - + return {'FINISHED'} + # Registration functions def register(): bpy.types.Scene.blendermcp_port = IntProperty( @@ -1692,12 +2712,12 @@ def register(): min=1024, max=65535 ) - + bpy.types.Scene.blendermcp_server_running = bpy.props.BoolProperty( name="Server Running", default=False ) - + bpy.types.Scene.blendermcp_use_polyhaven = bpy.props.BoolProperty( name="Use Poly Haven", description="Enable Poly Haven asset integration", @@ -1710,6 +2730,12 @@ def register(): default=False ) + bpy.types.Scene.blendermcp_use_geometry_nodes = bpy.props.BoolProperty( + name="Use Geometry Nodes", + description="Enable Geometry Nodes integration for procedural modeling", + default=False + ) + bpy.types.Scene.blendermcp_hyper3d_mode = bpy.props.EnumProperty( name="Rodin Mode", description="Choose the platform used to call Rodin APIs", @@ -1726,33 +2752,36 @@ def register(): description="API Key provided by Hyper3D", default="" ) - + bpy.utils.register_class(BLENDERMCP_PT_Panel) bpy.utils.register_class(BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey) bpy.utils.register_class(BLENDERMCP_OT_StartServer) bpy.utils.register_class(BLENDERMCP_OT_StopServer) - + print("BlenderMCP addon registered") + def unregister(): # Stop the server if it's running if hasattr(bpy.types, "blendermcp_server") and bpy.types.blendermcp_server: bpy.types.blendermcp_server.stop() del bpy.types.blendermcp_server - + bpy.utils.unregister_class(BLENDERMCP_PT_Panel) bpy.utils.unregister_class(BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey) bpy.utils.unregister_class(BLENDERMCP_OT_StartServer) bpy.utils.unregister_class(BLENDERMCP_OT_StopServer) - + del bpy.types.Scene.blendermcp_port del bpy.types.Scene.blendermcp_server_running del bpy.types.Scene.blendermcp_use_polyhaven del bpy.types.Scene.blendermcp_use_hyper3d + del bpy.types.Scene.blendermcp_use_geometry_nodes del bpy.types.Scene.blendermcp_hyper3d_mode del bpy.types.Scene.blendermcp_hyper3d_api_key print("BlenderMCP addon unregistered") + if __name__ == "__main__": register() diff --git a/src/blender_mcp/server.py b/src/blender_mcp/server.py index 09f795471..ef203429e 100644 --- a/src/blender_mcp/server.py +++ b/src/blender_mcp/server.py @@ -6,7 +6,7 @@ import logging from dataclasses import dataclass from contextlib import asynccontextmanager -from typing import AsyncIterator, Dict, Any, List +from typing import AsyncIterator, Dict, Any, List, Union import os from pathlib import Path import base64 @@ -201,10 +201,11 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: # Global connection for resources (since resources can't access context) _blender_connection = None _polyhaven_enabled = False # Add this global variable +_geometry_nodes_enabled = False # Geometry nodes feature status def get_blender_connection(): """Get or create a persistent Blender connection""" - global _blender_connection, _polyhaven_enabled # Add _polyhaven_enabled to globals + global _blender_connection, _polyhaven_enabled, _geometry_nodes_enabled # Add _geometry_nodes_enabled to globals # If we have an existing connection, check if it's still valid if _blender_connection is not None: @@ -213,6 +214,11 @@ def get_blender_connection(): result = _blender_connection.send_command("get_polyhaven_status") # Store the PolyHaven status globally _polyhaven_enabled = result.get("enabled", False) + + # Check if geometry nodes feature is enabled + geo_result = _blender_connection.send_command("get_geometry_nodes_status") + _geometry_nodes_enabled = geo_result.get("enabled", False) + return _blender_connection except Exception as e: # Connection is dead, close it and create a new one @@ -906,6 +912,19 @@ def asset_creation_strategy() -> str: Adjust the imported mesh's location, scale, rotation, so that the mesh is on the right spot. You can reuse assets previous generated by running python code to duplicate the object, without creating another generation task. + 3. Geometry Nodes + Use get_geometry_nodes_status() to verify the status of geometry nodes + If the geometry nodes feature is enabled, you must strictly follow these steps to create a node network: + 1. Query available nodes: Use get_node_info(output_format='text') to get an overview of all available nodes + 2. Get detailed information: Use get_node_info(node_type_name=['needed_nodes'], include_details=True, output_format='json') to get node details, including socket names and types + 3. Build network definition: + - Create nodes list: [{"type": "NodeType", "location": [x, y], "inputs": {}, "properties": {}, "label": "Label"}] + - Create links list: [{"from_node": source_node, "from_socket": "from_socket", "to_node": target_node, "to_socket": "to_socket"}] + - Use "input" and "output" as special identifiers to reference input/output nodes + 4. Direct creation: Use complete_geometry_node(object_name="name", nodes=nodes, links=links, input_sockets=input_sockets) + - No need to create objects in advance, the function will automatically create objects and modifiers + - The nodes list doesn't need to manually specify GroupNodeInput/Output as default input/output nodes will be created automatically + - You can specify input_sockets with default values that will be set automatically 2. If all integrations are disabled or when falling back to basic tools: - create_object() for basic primitives (CUBE, SPHERE, CYLINDER, etc.) @@ -929,6 +948,218 @@ def asset_creation_strategy() -> str: - The task specifically requires a basic material/color """ +@mcp.tool() +def get_node_info( + ctx: Context, + output_format: str = 'text', + include_details: bool = False, + node_type_name: str = '' +) -> str: + """ + Get information about Blender geometry node types, useful for understanding available node types and their properties. + + [IMPORTANT] Workflow that must be followed when using geometry nodes: + 1. First use get_node_info(output_format='text') to get an overview of all available nodes + 2. Then use get_node_info(node_type_name='required_node_type1,required_node_type2', include_details=True, output_format='json') to get detailed information + - Multiple node type names should be separated by commas + - Also supports passing a list of node type names + 3. Finally use complete_geometry_node() to create the node network + Never skip these steps when creating node networks, otherwise errors will occur. + + Parameters: + - output_format: Output format, options are 'text' or 'json' + - include_details: Whether to include detailed information (properties, input and output sockets) + - node_type_name: Specify node type name(s), can be: + - Single name string: "GeometryNodeMeshCube" + - Comma-separated list: "GeometryNodeMeshCube,GeometryNodeMeshSphere" + If not provided, information for all nodes will be returned + + Returns: + - 'text' format: Node information in text format, with one line per node containing name and description + - 'json' format: Node information in JSON format list + + Usage example: + # Step 1: Get an overview of all nodes + nodes_overview = get_node_info(output_format='text') + + # Step 2: Get detailed information for specific nodes (multiple nodes separated by commas) + node_details = get_node_info( + node_type_name='GeometryNodeMeshSphere,GeometryNodeDistributePointsOnFaces', + include_details=True, + output_format='json' + ) + """ + try: + blender = get_blender_connection() + + # Check if geometry nodes feature is enabled + if not _geometry_nodes_enabled: + return "Geometry nodes feature is disabled. Please enable this feature in the Blender MCP panel." + + # Ensure parameter types are correct + try: + # Process include_details parameter, ensure it's a boolean + if isinstance(include_details, str): + include_details_lower = include_details.lower() + include_details = include_details_lower == 'true' or include_details_lower == 'yes' or include_details_lower == '1' + + # Process node_type_name parameter, support comma-separated string + if node_type_name is not None: + # If it's a string, check if it's a comma-separated list + if isinstance(node_type_name, str): + # Check if it's a multi-node name with commas + if ',' in node_type_name: + # Split by comma and remove whitespace + node_type_name = [name.strip() for name in node_type_name.split(',') if name.strip()] + + # Make sure parameter type is correct + if not isinstance(node_type_name, (str, list)): + return f"Error: node_type_name parameter must be a string or list of strings, received: {type(node_type_name)}" + + logger.info(f"Getting node info: format={output_format}, details={include_details}, nodes={node_type_name}") + + # Ensure output_format is text or json + if output_format not in ('text', 'json'): + return f"Error: Unsupported output format: {output_format}, please use 'text' or 'json'" + + result = blender.send_command("get_node_info", { + "output_format": output_format, + "include_details": include_details, + "node_type_name": node_type_name + }) + + if isinstance(result, dict) and "error" in result: + return f"Error: {result['error']}" + + # Validate returned data + if result is None or len(str(result).strip()) == 0: + return "Error: Node information from Blender is empty" + + return result + except ValueError as e: + return f"Parameter error: {str(e)}" + except Exception as e: + logger.error(f"Error getting node info: {str(e)}") + return f"Error getting node info: {str(e)}" + +@mcp.tool() +def complete_geometry_node( + ctx: Context, + object_name: str, + nodes: list, + links: list, + input_sockets: list = None +) -> str: + """ + Create or complete a geometry node network for procedural modeling and geometry generation. (No need to set materials) + + [IMPORTANT] Geometry node creation workflow: + 1. Must first use get_node_info() to understand node types and properties + 2. When building nodes and links definitions, note: + - No need to manually create "Group Input" and "Group Output" nodes + - Use "input" and "output" as special identifiers to reference these automatically created nodes + - Example: {"from_node": "input", "from_socket": "Geometry", "to_node": 0, "to_socket": "Mesh"} + 3. No need to create objects in advance with create_object(), this function will automatically create them + 4. When nodes have the same socket names, use indices instead of names + + Parameters: + - object_name: Name of the object to apply geometry nodes to (will be created automatically) + - nodes: List of node definitions, each containing type, location, label, etc. + Format: [{"type": "NodeType", "location": [x, y], "label": "Label", "inputs": {...}, "properties": {...}}] + - links: List of node connections + Format: [{"from_node": source_node, "from_socket": source_socket, "to_node": target_node, "to_socket": target_socket}] + - input_sockets: Optional node group input interface definitions + Format: [{"name": "SocketName", "type": "SocketType", "value": default_value}] + - Socket types like: NodeSocketFloat, NodeSocketVector, etc. + - value is optional, different socket types support different value types: + * NodeSocketFloat: float number + * NodeSocketInt: integer + * NodeSocketVector: [x, y, z] list + * NodeSocketColor: [r, g, b, a] list + + Returns: + Creation result and status information + + Usage example: + ```python + # Use geometry nodes to create points distributed on a sphere surface with exposed radius and point density parameters + + # Define needed nodes + nodes = [ + # Create UV sphere + {"type": "GeometryNodeMeshUVSphere", "location": [-200, 0], "inputs": {"Segments": 32, "Rings": 16}}, + # Distribute points on sphere surface + {"type": "GeometryNodeDistributePointsOnFaces", "location": [0, 0], "properties": {"distribute_method": "POISSON"}} + ] + + # Define connections between nodes + links = [ + # Connect input parameters to nodes + {"from_node": "input", "from_socket": "Radius", "to_node": 0, "to_socket": "Radius"}, + {"from_node": "input", "from_socket": "Density", "to_node": 1, "to_socket": "Density Max"}, + + # Connect between nodes + {"from_node": 0, "from_socket": "Mesh", "to_node": 1, "to_socket": "Mesh"}, + + # Connect to output + {"from_node": 1, "from_socket": "Points", "to_node": "output", "to_socket": "Geometry"} + ] + + # Define input interfaces with default values + input_sockets = [ + {"name": "Geometry", "type": "NodeSocketGeometry"}, + {"name": "Radius", "type": "NodeSocketFloat", "value": 2.0}, + {"name": "Density", "type": "NodeSocketFloat", "value": 50.0} + ] + + # Create geometry nodes + result = complete_geometry_node( + object_name="SpherePoints", + nodes=nodes, + links=links, + input_sockets=input_sockets, + ) + ``` + + returns: + Detailed data containing operation status and created node network information + """ + try: + blender = get_blender_connection() + + # Check if geometry nodes feature is enabled + if not _geometry_nodes_enabled: + return "Geometry nodes feature is disabled. Please enable this feature in the Blender MCP panel." + + result = blender.send_command("complete_geometry_node", { + "object_name": object_name, + "nodes": nodes, + "links": links, + "input_sockets": input_sockets, + }) + + return json.dumps(result, indent=2) + except Exception as e: + logger.error(f"Error completing geometry node network: {str(e)}") + return f"Error completing geometry node network: {str(e)}" + +@mcp.tool() +def get_geometry_nodes_status(ctx: Context) -> str: + """ + Check if the geometry nodes feature is enabled in Blender. + Returns a message indicating whether geometry nodes features are available. + """ + try: + blender = get_blender_connection() + result = blender.send_command("get_geometry_nodes_status") + enabled = result.get("enabled", False) + message = result.get("message", "") + + return message + except Exception as e: + logger.error(f"Error checking geometry nodes status: {str(e)}") + return f"Error checking geometry nodes status: {str(e)}" + # Main execution def main(): @@ -936,4 +1167,4 @@ def main(): mcp.run() if __name__ == "__main__": - main() \ No newline at end of file + main() From a1e19b9baedc274a0bf4ccbfddd3b2618e8f714c Mon Sep 17 00:00:00 2001 From: Duke Date: Sun, 11 Jan 2026 12:37:45 +0800 Subject: [PATCH 2/4] chore: resolve conflicts in geo-node feature --- .gitignore | 6 +- README.md | 86 +- TERMS_AND_CONDITIONS.md | 172 ++++ addon.py | 1269 ++++++++++++++++++++++-- pyproject.toml | 4 +- src/blender_mcp/server.py | 672 ++++++++++--- src/blender_mcp/telemetry.py | 315 ++++++ src/blender_mcp/telemetry_decorator.py | 65 ++ uv.lock | 1012 +++++++++++++++++-- 9 files changed, 3299 insertions(+), 302 deletions(-) create mode 100644 TERMS_AND_CONDITIONS.md create mode 100644 src/blender_mcp/telemetry.py create mode 100644 src/blender_mcp/telemetry_decorator.py diff --git a/.gitignore b/.gitignore index 6ec78364a..39c26df6b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,8 @@ wheels/ # Virtual environments .venv -.idea/ \ No newline at end of file +# macOS +.DS_Store + +# Local config secrets +src/blender_mcp/config.py diff --git a/README.md b/README.md index 29de15c9f..ab4e12450 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ + + # BlenderMCP - Blender Model Context Protocol Integration BlenderMCP connects Blender to Claude AI through the Model Context Protocol (MCP), allowing Claude to directly interact with and control Blender. This integration enables prompt assisted 3D modeling, scene creation, and manipulation. +**We have no official website. Any website you see online is unofficial and has no affiliation with this project. Use them at your own risk.** + [Full tutorial](https://www.youtube.com/watch?v=lCyQ717DuzQ) ### Join the Community @@ -10,22 +14,32 @@ Give feedback, get inspired, and build on top of the MCP: [Discord](https://disc ### Supporters -**Top supporters:** - [CodeRabbit](https://www.coderabbit.ai/) +[Satish Goda](https://github.com/satishgoda) + **All supporters:** [Support this project](https://github.com/sponsors/ahujasid) -## Release notes (1.1.0) +## Release notes (1.4.0) +- Added Hunyuan3D support -- Added support for Poly Haven assets through their API -- Added support to prompt 3D models using Hyper3D Rodin + +### Previously added features: +- View screenshots for Blender viewport to better understand the scene +- Search and download Sketchfab models +- Support for Poly Haven assets through their API +- Support to generate 3D models using Hyper3D Rodin +- Run Blender MCP on a remote host +- Telemetry for tools executed (completely anonymous) + +### Installating a new version (existing users) - For newcomers, you can go straight to Installation. For existing users, see the points below - Download the latest addon.py file and replace the older one, then add it to Blender - Delete the MCP server from Claude and add it back again, and you should be good to go! + ## Features - **Two-way communication**: Connect Claude AI to Blender through a socket-based server @@ -55,18 +69,32 @@ The system consists of two main components: brew install uv ``` **On Windows** -```bash +```powershell powershell -c "irm https://astral.sh/uv/install.ps1 | iex" ``` -and then -```bash -set Path=C:\Users\nntra\.local\bin;%Path% +and then add uv to the user path in Windows (you may need to restart Claude Desktop after): +```powershell +$localBin = "$env:USERPROFILE\.local\bin" +$userPath = [Environment]::GetEnvironmentVariable("Path", "User") +[Environment]::SetEnvironmentVariable("Path", "$userPath;$localBin", "User") ``` Otherwise installation instructions are on their website: [Install uv](https://docs.astral.sh/uv/getting-started/installation/) **⚠️ Do not proceed before installing UV** +### Environment Variables + +The following environment variables can be used to configure the Blender connection: + +- `BLENDER_HOST`: Host address for Blender socket server (default: "localhost") +- `BLENDER_PORT`: Port number for Blender socket server (default: 9876) + +Example: +```bash +export BLENDER_HOST='host.docker.internal' +export BLENDER_PORT=9876 +``` ### Claude for Desktop Integration @@ -89,10 +117,25 @@ Go to Claude > Settings > Developer > Edit Config > claude_desktop_config.json t ### Cursor integration -Run blender-mcp without installing it permanently through uvx. Go to Cursor Settings > MCP and paste this as a command. +[![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=blender&config=eyJjb21tYW5kIjoidXZ4IGJsZW5kZXItbWNwIn0%3D) -```bash -uvx blender-mcp +For Mac users, go to Settings > MCP and paste the following + +- To use as a global server, use "add new global MCP server" button and paste +- To use as a project specific server, create `.cursor/mcp.json` in the root of the project and paste + + +```json +{ + "mcpServers": { + "blender": { + "command": "uvx", + "args": [ + "blender-mcp" + ] + } + } +} ``` For Windows users, go to Settings > MCP > Add Server, add a new server with the following settings: @@ -112,11 +155,16 @@ For Windows users, go to Settings > MCP > Add Server, add a new server with the } ``` - [Cursor setup video](https://www.youtube.com/watch?v=wgWsJshecac) **⚠️ Only run one instance of the MCP server (either on Cursor or Claude Desktop), not both** +### Visual Studio Code Integration + +_Prerequisites_: Make sure you have [Visual Studio Code](https://code.visualstudio.com/) installed before proceeding. + +[![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_blender--mcp_server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=ffffff)](vscode:mcp/install?%7B%22name%22%3A%22blender-mcp%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22blender-mcp%22%5D%7D) + ### Installing the Blender Addon 1. Download the `addon.py` file from this repo @@ -151,7 +199,6 @@ Once the config file has been set on Claude, and the addon is running on Blender - Execute any Python code in Blender - Download the right models, assets and HDRIs through [Poly Haven](https://polyhaven.com/) - AI generated 3D models through [Hyper3D Rodin](https://hyper3d.ai/) -- Geometry Node Module To Create procedural modeling and geometry generation ### Example Commands @@ -168,21 +215,10 @@ Here are some examples of what you can ask Claude to do: - "Make the lighting like a studio" - "Point the camera at the scene, and make it isometric" - -## Geometry Node Module Example Commands - -- "Create a simple procedural table" -- "Create a simple procedural chair" -- "Create a simple procedural car" -- "Use geometry nodes to create points distributed on a sphere surface with exposed radius and density parameters" -- "Use geometry nodes to create a noise-deformed sphere with exposed strength parameter" - - ## Hyper3D integration Hyper3D's free trial key allows you to generate a limited number of models per day. If the daily limit is reached, you can wait for the next day's reset or obtain your own key from hyper3d.ai and fal.ai. - ## Troubleshooting - **Connection issues**: Make sure the Blender addon server is running, and the MCP server is configured on Claude, DO NOT run the uvx command in the terminal. Sometimes, the first command won't go through but after that it starts working. diff --git a/TERMS_AND_CONDITIONS.md b/TERMS_AND_CONDITIONS.md new file mode 100644 index 000000000..e647dd028 --- /dev/null +++ b/TERMS_AND_CONDITIONS.md @@ -0,0 +1,172 @@ +# Blender MCP - Terms of Use and Privacy Policy + +**Last Updated: January 2025** + +--- + +## 1. About This Project + +Blender MCP is a free, open-source project maintained by Siddharth Ahuja ("I," "me," "my"). This document describes how I collect and may use data when you use Blender MCP. + +By using Blender MCP, you agree to these terms. If you do not agree, please do not use the software. + +--- + +## 2. Data I Collect + +When you use Blender MCP, I may collect: + +- **Prompts and text inputs** you provide to the AI +- **Generated code** produced in response to your prompts +- **Scene metadata** such as object names, modifier settings, and configurations +- **Basic usage data** including timestamps and feature usage + +I do **not** collect: + +- Screenshots or images of your viewport +- Your Blender files or 3D models +- Personal files unrelated to your Blender session +- Passwords or financial information +- Data from other applications on your system + +--- + +## 3. How I May Use Your Data + +I am currently collecting data for potential future use. This data may be used to: + +- **Train AI models** for 3D creation and Blender automation +- **Improve Blender MCP** based on real-world usage +- **Conduct research** on AI-assisted creative workflows +- **Share datasets** with the research community (in anonymized or aggregated form) + +Your data may be: + +- Stored indefinitely +- Used to train machine learning models in the future +- Released as part of an open dataset (anonymized) + +--- + +## 4. Data Sharing + +I may share collected data with: + +- **The open-source/research community** as part of public datasets +- **Collaborators** working on AI or Blender-related research +- **Legal authorities** if required by law + +I do not sell your data. + +--- + +## 5. Your Rights + +You may: + +- **Request access** to the data I've collected from your usage +- **Request deletion** of your data (if it hasn't yet been used for training or released publicly) +- **Opt out** by discontinuing use of Blender MCP + +To exercise these rights, contact me at ahujasid@gmail.com. + +**Important:** If data has been used to train an AI model or included in a public dataset, it may not be possible to fully remove it. + +--- + +## 6. Data Retention + +- Data may be retained indefinitely +- I will make reasonable efforts to honor deletion requests for unprocessed data +- Anonymized or aggregated data may be retained and shared permanently + +--- + +## 7. Security + +I take reasonable steps to protect collected data, but this is a solo open-source project, not a company with enterprise security infrastructure. I cannot guarantee absolute security. + +--- + +## 8. Children + +Blender MCP is not intended for users under 16. I do not knowingly collect data from children. + +--- + +## 9. International Users + +Your data may be stored and processed in any country. By using Blender MCP, you consent to international data transfers. + +--- + +## 10. Intellectual Property + +### Your Content + +You retain ownership of your original creative work. By using Blender MCP, you grant me a **worldwide, royalty-free, perpetual license** to use: + +- Prompts you submit +- Code generated in response to your prompts +- Scene metadata captured during use + +This license is for AI training, research, open datasets, and improving the project. + +### AI-Generated Content + +You may use AI-generated code however you like, but it's provided "as is" with no guarantees. + +### Blender MCP + +The Blender MCP source code is open source under its stated license. These terms apply only to data collection. + +--- + +## 11. No Warranty + +BLENDER MCP IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES. + +I do not guarantee that: + +- The software will work correctly +- AI-generated code will be safe or functional +- Your data will be secure + +**You are responsible for reviewing any AI-generated code before using it.** + +--- + +## 12. Limitation of Liability + +TO THE MAXIMUM EXTENT PERMITTED BY LAW, I AM NOT LIABLE FOR ANY DAMAGES ARISING FROM YOUR USE OF BLENDER MCP. + +This is a free, open-source project maintained in my spare time. Use at your own risk. + +--- + +## 13. Changes + +I may update these terms at any time. Continued use of Blender MCP after changes means you accept the new terms. + +--- + +## 14. Contact + +Questions or requests? Email me at ahujasid@gmail.com. + +--- + +## 15. Consent + +By using Blender MCP, you acknowledge that: + +1. You have read and understood these terms +2. You consent to the collection of prompts, generated code, and scene metadata +3. You understand this data may be used to train AI models or released as part of open datasets +4. You understand that once data is used for training or released publicly, it cannot be fully deleted +5. You are at least 16 years old + +--- + +*Blender MCP is an independent project and is not affiliated with the Blender Foundation.* + diff --git a/addon.py b/addon.py index b41868750..444fdcd32 100644 --- a/addon.py +++ b/addon.py @@ -1,5 +1,6 @@ # Code created by Siddharth Ahuja: www.github.com/ahujasid © 2025 +import re import bpy import mathutils import json @@ -11,7 +12,13 @@ import traceback import os import shutil +import zipfile from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty +import io +from datetime import datetime +import hashlib, hmac, base64 +import os.path as osp +from contextlib import redirect_stdout, suppress from dataclasses import dataclass, field from typing import List, Dict, Union, Any, Optional, Tuple @@ -27,10 +34,13 @@ RODIN_FREE_TRIAL_KEY = "k9TcfFoEhNd9cCPP2guHAHHHkctZHIRhZDywZ1euGUXwihbYLpOjQhofby80NJez" +# Add User-Agent as required by Poly Haven API +REQ_HEADERS = requests.utils.default_headers() +REQ_HEADERS.update({"User-Agent": "blender-mcp"}) + # region GeometryNodeDataClass IS_BLENDER_4 = bpy.app.version[0] >= 4 - @dataclass class NodeDefinition: """Node definition data class""" @@ -40,7 +50,6 @@ class NodeDefinition: inputs: Dict[str, Any] = field(default_factory=dict) # Input values dictionary properties: Dict[str, Any] = field(default_factory=dict) # Node properties parameter dictionary - @dataclass class NodeLink: """Node connection data class""" @@ -49,7 +58,6 @@ class NodeLink: to_node: Union[str, int] # Target node name or index to_socket: Union[str, int] # Target socket name or index - @dataclass class GeometryNodeNetwork: """Geometry node network data class""" @@ -59,7 +67,6 @@ class GeometryNodeNetwork: input_sockets: List[Dict[str, str]] = field(default_factory=list) # Input interface definition output_sockets: List[Dict[str, str]] = field(default_factory=list) # Output interface definition - @dataclass class SocketInfo: """Socket information data class""" @@ -71,7 +78,6 @@ class SocketInfo: hide: bool # Whether hidden default_value: Any = None # Default value (if any) - @dataclass class PropertyInfo: """Node property information data class""" @@ -82,7 +88,6 @@ class PropertyInfo: default_value: Any = None # Default value (if any) enum_items: List[Dict[str, str]] = field(default_factory=list) # Enum options (if any) - @dataclass class NodeInfo: """Node information data class""" @@ -92,9 +97,9 @@ class NodeInfo: outputs: List[SocketInfo] = field(default_factory=list) # Output socket information properties: List[PropertyInfo] = field(default_factory=list) # Node property information - # endregion + class BlenderMCPServer: def __init__(self, host='localhost', port=9876): self.host = host @@ -244,8 +249,9 @@ def execute_wrapper(): def execute_command(self, command): """Execute a command in the main Blender thread""" - try: + try: return self._execute_command_internal(command) + except Exception as e: print(f"Error executing command: {str(e)}") traceback.print_exc() @@ -259,11 +265,6 @@ def _execute_command_internal(self, command): # Add a handler for checking PolyHaven status if cmd_type == "get_polyhaven_status": return {"status": "success", "result": self.get_polyhaven_status()} - - # Add a handler for checking Hyper3D status - if cmd_type == "get_hyper3d_status": - return {"status": "success", "result": self.get_hyper3d_status()} - # Add a handler for checking GeometryNodes status if cmd_type == "get_geometry_nodes_status": return {"status": "success", "result": self.get_geometry_nodes_status()} @@ -272,9 +273,13 @@ def _execute_command_internal(self, command): handlers = { "get_scene_info": self.get_scene_info, "get_object_info": self.get_object_info, + "get_viewport_screenshot": self.get_viewport_screenshot, "execute_code": self.execute_code, + "get_telemetry_consent": self.get_telemetry_consent, "get_polyhaven_status": self.get_polyhaven_status, "get_hyper3d_status": self.get_hyper3d_status, + "get_sketchfab_status": self.get_sketchfab_status, + "get_hunyuan3d_status": self.get_hunyuan3d_status, "get_geometry_nodes_status": self.get_geometry_nodes_status, } @@ -290,12 +295,30 @@ def _execute_command_internal(self, command): # Add Hyper3d handlers only if enabled if bpy.context.scene.blendermcp_use_hyper3d: - hyper3d_handlers = { + polyhaven_handlers = { "create_rodin_job": self.create_rodin_job, "poll_rodin_job_status": self.poll_rodin_job_status, "import_generated_asset": self.import_generated_asset, } - handlers.update(hyper3d_handlers) + handlers.update(polyhaven_handlers) + + # Add Sketchfab handlers only if enabled + if bpy.context.scene.blendermcp_use_sketchfab: + sketchfab_handlers = { + "search_sketchfab_models": self.search_sketchfab_models, + "get_sketchfab_model_preview": self.get_sketchfab_model_preview, + "download_sketchfab_model": self.download_sketchfab_model, + } + handlers.update(sketchfab_handlers) + + # Add Hunyuan3d handlers only if enabled + if bpy.context.scene.blendermcp_use_hunyuan3d: + hunyuan_handlers = { + "create_hunyuan_job": self.create_hunyuan_job, + "poll_hunyuan_job_status": self.poll_hunyuan_job_status, + "import_generated_asset_hunyuan": self.import_generated_asset_hunyuan + } + handlers.update(hunyuan_handlers) # Add geometry nodes handlers only if enabled if bpy.context.scene.blendermcp_use_geometry_nodes: @@ -320,6 +343,7 @@ def _execute_command_internal(self, command): return {"status": "error", "message": f"Unknown command type: {cmd_type}"} + def get_scene_info(self): """Get information about the current Blender scene""" try: @@ -342,8 +366,8 @@ def get_scene_info(self): "type": obj.type, # Only include basic location data "location": [round(float(obj.location.x), 2), - round(float(obj.location.y), 2), - round(float(obj.location.z), 2)], + round(float(obj.location.y), 2), + round(float(obj.location.z), 2)], } scene_info["objects"].append(obj_info) @@ -375,6 +399,7 @@ def _get_aabb(obj): ] + def get_object_info(self, name): """Get detailed information about a specific object""" obj = bpy.data.objects.get(name) @@ -412,24 +437,89 @@ def get_object_info(self, name): return obj_info + def get_viewport_screenshot(self, max_size=800, filepath=None, format="png"): + """ + Capture a screenshot of the current 3D viewport and save it to the specified path. + + Parameters: + - max_size: Maximum size in pixels for the largest dimension of the image + - filepath: Path where to save the screenshot file + - format: Image format (png, jpg, etc.) + + Returns success/error status + """ + try: + if not filepath: + return {"error": "No filepath provided"} + + # Find the active 3D viewport + area = None + for a in bpy.context.screen.areas: + if a.type == 'VIEW_3D': + area = a + break + + if not area: + return {"error": "No 3D viewport found"} + + # Take screenshot with proper context override + with bpy.context.temp_override(area=area): + bpy.ops.screen.screenshot_area(filepath=filepath) + + # Load and resize if needed + img = bpy.data.images.load(filepath) + width, height = img.size + + if max(width, height) > max_size: + scale = max_size / max(width, height) + new_width = int(width * scale) + new_height = int(height * scale) + img.scale(new_width, new_height) + + # Set format and save + img.file_format = format.upper() + img.save() + width, height = new_width, new_height + + # Cleanup Blender image data + bpy.data.images.remove(img) + + return { + "success": True, + "width": width, + "height": height, + "filepath": filepath + } + + except Exception as e: + return {"error": str(e)} + def execute_code(self, code): """Execute arbitrary Blender Python code""" # This is powerful but potentially dangerous - use with caution try: # Create a local namespace for execution namespace = {"bpy": bpy} - exec(code, namespace) - return {"executed": True} + + # Capture stdout during execution, and return it as result + capture_buffer = io.StringIO() + with redirect_stdout(capture_buffer): + exec(code, namespace) + + captured_output = capture_buffer.getvalue() + return {"executed": True, "result": captured_output} except Exception as e: raise Exception(f"Code execution error: {str(e)}") + + def get_polyhaven_categories(self, asset_type): """Get categories for a specific asset type from Polyhaven""" try: if asset_type not in ["hdris", "textures", "models", "all"]: return {"error": f"Invalid asset type: {asset_type}. Must be one of: hdris, textures, models, all"} - response = requests.get(f"https://api.polyhaven.com/categories/{asset_type}") + response = requests.get(f"https://api.polyhaven.com/categories/{asset_type}", headers=REQ_HEADERS) if response.status_code == 200: return {"categories": response.json()} else: @@ -451,7 +541,7 @@ def search_polyhaven_assets(self, asset_type=None, categories=None): if categories: params["categories"] = categories - response = requests.get(url, params=params) + response = requests.get(url, params=params, headers=REQ_HEADERS) if response.status_code == 200: # Limit the response size to avoid overwhelming Blender assets = response.json() @@ -471,7 +561,7 @@ def search_polyhaven_assets(self, asset_type=None, categories=None): def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_format=None): try: # First get the files information - files_response = requests.get(f"https://api.polyhaven.com/files/{asset_id}") + files_response = requests.get(f"https://api.polyhaven.com/files/{asset_id}", headers=REQ_HEADERS) if files_response.status_code != 200: return {"error": f"Failed to get asset files: {files_response.status_code}"} @@ -483,8 +573,7 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f if not file_format: file_format = "hdr" # Default format for HDRIs - if "hdri" in files_data and resolution in files_data["hdri"] and file_format in files_data["hdri"][ - resolution]: + if "hdri" in files_data and resolution in files_data["hdri"] and file_format in files_data["hdri"][resolution]: file_info = files_data["hdri"][resolution][file_format] file_url = file_info["url"] @@ -492,7 +581,7 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f # since Blender can't properly load HDR data directly from memory with tempfile.NamedTemporaryFile(suffix=f".{file_format}", delete=False) as tmp_file: # Download the file - response = requests.get(file_url) + response = requests.get(file_url, headers=REQ_HEADERS) if response.status_code != 200: return {"error": f"Failed to download HDRI: {response.status_code}"} @@ -588,7 +677,7 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f # Use NamedTemporaryFile like we do for HDRIs with tempfile.NamedTemporaryFile(suffix=f".{file_format}", delete=False) as tmp_file: # Download the file - response = requests.get(file_url) + response = requests.get(file_url, headers=REQ_HEADERS) if response.status_code == 200: tmp_file.write(response.content) tmp_path = tmp_file.name @@ -725,7 +814,7 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f main_file_name = file_url.split("/")[-1] main_file_path = os.path.join(temp_dir, main_file_name) - response = requests.get(file_url) + response = requests.get(file_url, headers=REQ_HEADERS) if response.status_code != 200: return {"error": f"Failed to download model: {response.status_code}"} @@ -743,7 +832,7 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f os.makedirs(os.path.dirname(include_file_path), exist_ok=True) # Download the included file - include_response = requests.get(include_url) + include_response = requests.get(include_url, headers=REQ_HEADERS) if include_response.status_code == 200: with open(include_file_path, "wb") as f: f.write(include_response.content) @@ -781,10 +870,8 @@ def download_polyhaven_asset(self, asset_id, asset_type, resolution="1k", file_f return {"error": f"Failed to import model: {str(e)}"} finally: # Clean up temporary directory - try: + with suppress(Exception): shutil.rmtree(temp_dir) - except: - print(f"Failed to clean up temporary directory: {temp_dir}") else: return {"error": f"Requested format or resolution not available for this model"} @@ -1098,6 +1185,21 @@ def set_texture(self, object_name, texture_id): traceback.print_exc() return {"error": f"Failed to apply texture: {str(e)}"} + def get_telemetry_consent(self): + """Get the current telemetry consent status""" + try: + # Get addon preferences - use the module name + addon_prefs = bpy.context.preferences.addons.get(__name__) + if addon_prefs: + consent = addon_prefs.preferences.telemetry_consent + else: + # Fallback to default if preferences not available + consent = True + except (AttributeError, KeyError): + # Fallback to default if preferences not available + consent = True + return {"consent": consent} + def get_polyhaven_status(self): """Get the current status of PolyHaven integration""" enabled = bpy.context.scene.blendermcp_use_polyhaven @@ -1110,9 +1212,40 @@ def get_polyhaven_status(self): 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) 2. Check the 'Use assets from Poly Haven' checkbox 3. Restart the connection to Claude""" + } + + + def get_geometry_nodes_status(self): + """Get the status of the geometry nodes feature in Blender. + + Returns: + dict: A dictionary containing information about the geometry nodes feature status. + """ + try: + # Get the geometry nodes setting from Blender preferences + enabled = bpy.context.scene.blendermcp_use_geometry_nodes + if enabled: + return { + "status": "success", + "enabled": True, + "message": "Geometry nodes feature is enabled." + } + else: + return { + "status": "warning", + "enabled": False, + "message": "Geometry nodes feature is disabled. Enable it in the BlenderMCP panel to use geometry node commands." + } + except Exception as e: + # Handle errors when checking status + return { + "status": "error", + "enabled": False, + "message": f"Error checking geometry nodes status: {str(e)}" } - # region Hyper3D + + #region Hyper3D def get_hyper3d_status(self): """Get the current status of Hyper3D Rodin integration""" enabled = bpy.context.scene.blendermcp_use_hyper3d @@ -1128,7 +1261,7 @@ def get_hyper3d_status(self): } mode = bpy.context.scene.blendermcp_hyper3d_mode message = f"Hyper3D Rodin integration is enabled and ready to use. Mode: {mode}. " + \ - f"Key type: {'private' if bpy.context.scene.blendermcp_hyper3d_api_key != RODIN_FREE_TRIAL_KEY else 'free_trial'}" + f"Key type: {'private' if bpy.context.scene.blendermcp_hyper3d_api_key != RODIN_FREE_TRIAL_KEY else 'free_trial'}" return { "enabled": True, "message": message @@ -1153,10 +1286,10 @@ def create_rodin_job(self, *args, **kwargs): def create_rodin_job_main_site( self, - text_prompt: str = None, - images: list[tuple[str, str]] = None, + text_prompt: str=None, + images: list[tuple[str, str]]=None, bbox_condition=None - ): + ): try: if images is None: images = [] @@ -1184,10 +1317,10 @@ def create_rodin_job_main_site( def create_rodin_job_fal_ai( self, - text_prompt: str = None, - images: list[tuple[str, str]] = None, + text_prompt: str=None, + images: list[tuple[str, str]]=None, bbox_condition=None - ): + ): try: req_data = { "tier": "Sketch", @@ -1283,14 +1416,14 @@ def _clean_imported_glb(filepath, mesh_name=None): potential_mesh = parent_obj.children[0] if potential_mesh.type == 'MESH': print("GLB structure confirmed: Empty node with one mesh child.") - + # Unparent the mesh from the empty node potential_mesh.parent = None - + # Remove the empty node bpy.data.objects.remove(parent_obj) print("Removed empty node, keeping only the mesh.") - + mesh_obj = potential_mesh else: print("Error: Child is not a mesh object.") @@ -1446,8 +1579,855 @@ def import_generated_asset_fal_ai(self, request_id: str, name: str): } except Exception as e: return {"succeed": False, "error": str(e)} + #endregion + + #region Sketchfab API + def get_sketchfab_status(self): + """Get the current status of Sketchfab integration""" + enabled = bpy.context.scene.blendermcp_use_sketchfab + api_key = bpy.context.scene.blendermcp_sketchfab_api_key + + # Test the API key if present + if api_key: + try: + headers = { + "Authorization": f"Token {api_key}" + } + + response = requests.get( + "https://api.sketchfab.com/v3/me", + headers=headers, + timeout=30 # Add timeout of 30 seconds + ) + + if response.status_code == 200: + user_data = response.json() + username = user_data.get("username", "Unknown user") + return { + "enabled": True, + "message": f"Sketchfab integration is enabled and ready to use. Logged in as: {username}" + } + else: + return { + "enabled": False, + "message": f"Sketchfab API key seems invalid. Status code: {response.status_code}" + } + except requests.exceptions.Timeout: + return { + "enabled": False, + "message": "Timeout connecting to Sketchfab API. Check your internet connection." + } + except Exception as e: + return { + "enabled": False, + "message": f"Error testing Sketchfab API key: {str(e)}" + } + + if enabled and api_key: + return {"enabled": True, "message": "Sketchfab integration is enabled and ready to use."} + elif enabled and not api_key: + return { + "enabled": False, + "message": """Sketchfab integration is currently enabled, but API key is not given. To enable it: + 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) + 2. Keep the 'Use Sketchfab' checkbox checked + 3. Enter your Sketchfab API Key + 4. Restart the connection to Claude""" + } + else: + return { + "enabled": False, + "message": """Sketchfab integration is currently disabled. To enable it: + 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) + 2. Check the 'Use assets from Sketchfab' checkbox + 3. Enter your Sketchfab API Key + 4. Restart the connection to Claude""" + } + + def search_sketchfab_models(self, query, categories=None, count=20, downloadable=True): + """Search for models on Sketchfab based on query and optional filters""" + try: + api_key = bpy.context.scene.blendermcp_sketchfab_api_key + if not api_key: + return {"error": "Sketchfab API key is not configured"} + + # Build search parameters with exact fields from Sketchfab API docs + params = { + "type": "models", + "q": query, + "count": count, + "downloadable": downloadable, + "archives_flavours": False + } + + if categories: + params["categories"] = categories + + # Make API request to Sketchfab search endpoint + # The proper format according to Sketchfab API docs for API key auth + headers = { + "Authorization": f"Token {api_key}" + } + + + # Use the search endpoint as specified in the API documentation + response = requests.get( + "https://api.sketchfab.com/v3/search", + headers=headers, + params=params, + timeout=30 # Add timeout of 30 seconds + ) + + if response.status_code == 401: + return {"error": "Authentication failed (401). Check your API key."} + + if response.status_code != 200: + return {"error": f"API request failed with status code {response.status_code}"} + + response_data = response.json() + + # Safety check on the response structure + if response_data is None: + return {"error": "Received empty response from Sketchfab API"} + + # Handle 'results' potentially missing from response + results = response_data.get("results", []) + if not isinstance(results, list): + return {"error": f"Unexpected response format from Sketchfab API: {response_data}"} + + return response_data + + except requests.exceptions.Timeout: + return {"error": "Request timed out. Check your internet connection."} + except json.JSONDecodeError as e: + return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"} + except Exception as e: + import traceback + traceback.print_exc() + return {"error": str(e)} + + def get_sketchfab_model_preview(self, uid): + """Get thumbnail preview image of a Sketchfab model by its UID""" + try: + import base64 + + api_key = bpy.context.scene.blendermcp_sketchfab_api_key + if not api_key: + return {"error": "Sketchfab API key is not configured"} + + headers = {"Authorization": f"Token {api_key}"} + + # Get model info which includes thumbnails + response = requests.get( + f"https://api.sketchfab.com/v3/models/{uid}", + headers=headers, + timeout=30 + ) + + if response.status_code == 401: + return {"error": "Authentication failed (401). Check your API key."} + + if response.status_code == 404: + return {"error": f"Model not found: {uid}"} + + if response.status_code != 200: + return {"error": f"Failed to get model info: {response.status_code}"} + + data = response.json() + thumbnails = data.get("thumbnails", {}).get("images", []) + + if not thumbnails: + return {"error": "No thumbnail available for this model"} + + # Find a suitable thumbnail (prefer medium size ~640px) + selected_thumbnail = None + for thumb in thumbnails: + width = thumb.get("width", 0) + if 400 <= width <= 800: + selected_thumbnail = thumb + break + + # Fallback to the first available thumbnail + if not selected_thumbnail: + selected_thumbnail = thumbnails[0] + + thumbnail_url = selected_thumbnail.get("url") + if not thumbnail_url: + return {"error": "Thumbnail URL not found"} + + # Download the thumbnail image + img_response = requests.get(thumbnail_url, timeout=30) + if img_response.status_code != 200: + return {"error": f"Failed to download thumbnail: {img_response.status_code}"} + + # Encode image as base64 + image_data = base64.b64encode(img_response.content).decode('ascii') + + # Determine format from content type or URL + content_type = img_response.headers.get("Content-Type", "") + if "png" in content_type or thumbnail_url.endswith(".png"): + img_format = "png" + else: + img_format = "jpeg" + + # Get additional model info for context + model_name = data.get("name", "Unknown") + author = data.get("user", {}).get("username", "Unknown") + + return { + "success": True, + "image_data": image_data, + "format": img_format, + "model_name": model_name, + "author": author, + "uid": uid, + "thumbnail_width": selected_thumbnail.get("width"), + "thumbnail_height": selected_thumbnail.get("height") + } + + except requests.exceptions.Timeout: + return {"error": "Request timed out. Check your internet connection."} + except Exception as e: + import traceback + traceback.print_exc() + return {"error": f"Failed to get model preview: {str(e)}"} + + def download_sketchfab_model(self, uid, normalize_size=False, target_size=1.0): + """Download a model from Sketchfab by its UID + + Parameters: + - uid: The unique identifier of the Sketchfab model + - normalize_size: If True, scale the model so its largest dimension equals target_size + - target_size: The target size in Blender units (meters) for the largest dimension + """ + try: + api_key = bpy.context.scene.blendermcp_sketchfab_api_key + if not api_key: + return {"error": "Sketchfab API key is not configured"} + + # Use proper authorization header for API key auth + headers = { + "Authorization": f"Token {api_key}" + } + + # Request download URL using the exact endpoint from the documentation + download_endpoint = f"https://api.sketchfab.com/v3/models/{uid}/download" + + response = requests.get( + download_endpoint, + headers=headers, + timeout=30 # Add timeout of 30 seconds + ) + + if response.status_code == 401: + return {"error": "Authentication failed (401). Check your API key."} + + if response.status_code != 200: + return {"error": f"Download request failed with status code {response.status_code}"} + + data = response.json() + + # Safety check for None data + if data is None: + return {"error": "Received empty response from Sketchfab API for download request"} + + # Extract download URL with safety checks + gltf_data = data.get("gltf") + if not gltf_data: + return {"error": "No gltf download URL available for this model. Response: " + str(data)} + + download_url = gltf_data.get("url") + if not download_url: + return {"error": "No download URL available for this model. Make sure the model is downloadable and you have access."} + + # Download the model (already has timeout) + model_response = requests.get(download_url, timeout=60) # 60 second timeout + + if model_response.status_code != 200: + return {"error": f"Model download failed with status code {model_response.status_code}"} + + # Save to temporary file + temp_dir = tempfile.mkdtemp() + zip_file_path = os.path.join(temp_dir, f"{uid}.zip") + + with open(zip_file_path, "wb") as f: + f.write(model_response.content) + + # Extract the zip file with enhanced security + with zipfile.ZipFile(zip_file_path, 'r') as zip_ref: + # More secure zip slip prevention + for file_info in zip_ref.infolist(): + # Get the path of the file + file_path = file_info.filename + + # Convert directory separators to the current OS style + # This handles both / and \ in zip entries + target_path = os.path.join(temp_dir, os.path.normpath(file_path)) + + # Get absolute paths for comparison + abs_temp_dir = os.path.abspath(temp_dir) + abs_target_path = os.path.abspath(target_path) + + # Ensure the normalized path doesn't escape the target directory + if not abs_target_path.startswith(abs_temp_dir): + with suppress(Exception): + shutil.rmtree(temp_dir) + return {"error": "Security issue: Zip contains files with path traversal attempt"} + + # Additional explicit check for directory traversal + if ".." in file_path: + with suppress(Exception): + shutil.rmtree(temp_dir) + return {"error": "Security issue: Zip contains files with directory traversal sequence"} + + # If all files passed security checks, extract them + zip_ref.extractall(temp_dir) + + # Find the main glTF file + gltf_files = [f for f in os.listdir(temp_dir) if f.endswith('.gltf') or f.endswith('.glb')] + + if not gltf_files: + with suppress(Exception): + shutil.rmtree(temp_dir) + return {"error": "No glTF file found in the downloaded model"} + + main_file = os.path.join(temp_dir, gltf_files[0]) + + # Import the model + bpy.ops.import_scene.gltf(filepath=main_file) + + # Get the imported objects + imported_objects = list(bpy.context.selected_objects) + imported_object_names = [obj.name for obj in imported_objects] + + # Clean up temporary files + with suppress(Exception): + shutil.rmtree(temp_dir) + + # Find root objects (objects without parents in the imported set) + root_objects = [obj for obj in imported_objects if obj.parent is None] + + # Helper function to recursively get all mesh children + def get_all_mesh_children(obj): + """Recursively collect all mesh objects in the hierarchy""" + meshes = [] + if obj.type == 'MESH': + meshes.append(obj) + for child in obj.children: + meshes.extend(get_all_mesh_children(child)) + return meshes + + # Collect ALL meshes from the entire hierarchy (starting from roots) + all_meshes = [] + for obj in root_objects: + all_meshes.extend(get_all_mesh_children(obj)) + + if all_meshes: + # Calculate combined world bounding box for all meshes + all_min = mathutils.Vector((float('inf'), float('inf'), float('inf'))) + all_max = mathutils.Vector((float('-inf'), float('-inf'), float('-inf'))) + + for mesh_obj in all_meshes: + # Get world-space bounding box corners + for corner in mesh_obj.bound_box: + world_corner = mesh_obj.matrix_world @ mathutils.Vector(corner) + all_min.x = min(all_min.x, world_corner.x) + all_min.y = min(all_min.y, world_corner.y) + all_min.z = min(all_min.z, world_corner.z) + all_max.x = max(all_max.x, world_corner.x) + all_max.y = max(all_max.y, world_corner.y) + all_max.z = max(all_max.z, world_corner.z) + + # Calculate dimensions + dimensions = [ + all_max.x - all_min.x, + all_max.y - all_min.y, + all_max.z - all_min.z + ] + max_dimension = max(dimensions) + + # Apply normalization if requested + scale_applied = 1.0 + if normalize_size and max_dimension > 0: + scale_factor = target_size / max_dimension + scale_applied = scale_factor + + # ✅ Only apply scale to ROOT objects (not children!) + # Child objects inherit parent's scale through matrix_world + for root in root_objects: + root.scale = ( + root.scale.x * scale_factor, + root.scale.y * scale_factor, + root.scale.z * scale_factor + ) + + # Update the scene to recalculate matrix_world for all objects + bpy.context.view_layer.update() + + # Recalculate bounding box after scaling + all_min = mathutils.Vector((float('inf'), float('inf'), float('inf'))) + all_max = mathutils.Vector((float('-inf'), float('-inf'), float('-inf'))) + + for mesh_obj in all_meshes: + for corner in mesh_obj.bound_box: + world_corner = mesh_obj.matrix_world @ mathutils.Vector(corner) + all_min.x = min(all_min.x, world_corner.x) + all_min.y = min(all_min.y, world_corner.y) + all_min.z = min(all_min.z, world_corner.z) + all_max.x = max(all_max.x, world_corner.x) + all_max.y = max(all_max.y, world_corner.y) + all_max.z = max(all_max.z, world_corner.z) + + dimensions = [ + all_max.x - all_min.x, + all_max.y - all_min.y, + all_max.z - all_min.z + ] + + world_bounding_box = [[all_min.x, all_min.y, all_min.z], [all_max.x, all_max.y, all_max.z]] + else: + world_bounding_box = None + dimensions = None + scale_applied = 1.0 + + result = { + "success": True, + "message": "Model imported successfully", + "imported_objects": imported_object_names + } + + if world_bounding_box: + result["world_bounding_box"] = world_bounding_box + if dimensions: + result["dimensions"] = [round(d, 4) for d in dimensions] + if normalize_size: + result["scale_applied"] = round(scale_applied, 6) + result["normalized"] = True + + return result + + except requests.exceptions.Timeout: + return {"error": "Request timed out. Check your internet connection and try again with a simpler model."} + except json.JSONDecodeError as e: + return {"error": f"Invalid JSON response from Sketchfab API: {str(e)}"} + except Exception as e: + import traceback + traceback.print_exc() + return {"error": f"Failed to download model: {str(e)}"} + #endregion + + #region Hunyuan3D + def get_hunyuan3d_status(self): + """Get the current status of Hunyuan3D integration""" + enabled = bpy.context.scene.blendermcp_use_hunyuan3d + hunyuan3d_mode = bpy.context.scene.blendermcp_hunyuan3d_mode + if enabled: + match hunyuan3d_mode: + case "OFFICIAL_API": + if not bpy.context.scene.blendermcp_hunyuan3d_secret_id or not bpy.context.scene.blendermcp_hunyuan3d_secret_key: + return { + "enabled": False, + "mode": hunyuan3d_mode, + "message": """Hunyuan3D integration is currently enabled, but SecretId or SecretKey is not given. To enable it: + 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) + 2. Keep the 'Use Tencent Hunyuan 3D model generation' checkbox checked + 3. Choose the right platform and fill in the SecretId and SecretKey + 4. Restart the connection to Claude""" + } + case "LOCAL_API": + if not bpy.context.scene.blendermcp_hunyuan3d_api_url: + return { + "enabled": False, + "mode": hunyuan3d_mode, + "message": """Hunyuan3D integration is currently enabled, but API URL is not given. To enable it: + 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) + 2. Keep the 'Use Tencent Hunyuan 3D model generation' checkbox checked + 3. Choose the right platform and fill in the API URL + 4. Restart the connection to Claude""" + } + case _: + return { + "enabled": False, + "message": "Hunyuan3D integration is enabled and mode is not supported." + } + return { + "enabled": True, + "mode": hunyuan3d_mode, + "message": "Hunyuan3D integration is enabled and ready to use." + } + return { + "enabled": False, + "message": """Hunyuan3D integration is currently disabled. To enable it: + 1. In the 3D Viewport, find the BlenderMCP panel in the sidebar (press N if hidden) + 2. Check the 'Use Tencent Hunyuan 3D model generation' checkbox + 3. Restart the connection to Claude""" + } + + @staticmethod + def get_tencent_cloud_sign_headers( + method: str, + path: str, + headParams: dict, + data: dict, + service: str, + region: str, + secret_id: str, + secret_key: str, + host: str = None + ): + """Generate the signature header required for Tencent Cloud API requests headers""" + # Generate timestamp + timestamp = int(time.time()) + date = datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d") + + # If host is not provided, it is generated based on service and region. + if not host: + host = f"{service}.tencentcloudapi.com" + + endpoint = f"https://{host}" + + # Constructing the request body + payload_str = json.dumps(data) + + # ************* Step 1: Concatenate the canonical request string ************* + canonical_uri = path + canonical_querystring = "" + ct = "application/json; charset=utf-8" + canonical_headers = f"content-type:{ct}\nhost:{host}\nx-tc-action:{headParams.get('Action', '').lower()}\n" + signed_headers = "content-type;host;x-tc-action" + hashed_request_payload = hashlib.sha256(payload_str.encode("utf-8")).hexdigest() + + canonical_request = (method + "\n" + + canonical_uri + "\n" + + canonical_querystring + "\n" + + canonical_headers + "\n" + + signed_headers + "\n" + + hashed_request_payload) + + # ************* Step 2: Construct the reception signature string ************* + credential_scope = f"{date}/{service}/tc3_request" + hashed_canonical_request = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest() + string_to_sign = ("TC3-HMAC-SHA256" + "\n" + + str(timestamp) + "\n" + + credential_scope + "\n" + + hashed_canonical_request) + + # ************* Step 3: Calculate the signature ************* + def sign(key, msg): + return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() + + secret_date = sign(("TC3" + secret_key).encode("utf-8"), date) + secret_service = sign(secret_date, service) + secret_signing = sign(secret_service, "tc3_request") + signature = hmac.new( + secret_signing, + string_to_sign.encode("utf-8"), + hashlib.sha256 + ).hexdigest() + + # ************* Step 4: Connect Authorization ************* + authorization = ("TC3-HMAC-SHA256" + " " + + "Credential=" + secret_id + "/" + credential_scope + ", " + + "SignedHeaders=" + signed_headers + ", " + + "Signature=" + signature) + + # Constructing request headers + headers = { + "Authorization": authorization, + "Content-Type": "application/json; charset=utf-8", + "Host": host, + "X-TC-Action": headParams.get("Action", ""), + "X-TC-Timestamp": str(timestamp), + "X-TC-Version": headParams.get("Version", ""), + "X-TC-Region": region + } + + return headers, endpoint + + def create_hunyuan_job(self, *args, **kwargs): + match bpy.context.scene.blendermcp_hunyuan3d_mode: + case "OFFICIAL_API": + return self.create_hunyuan_job_main_site(*args, **kwargs) + case "LOCAL_API": + return self.create_hunyuan_job_local_site(*args, **kwargs) + case _: + return f"Error: Unknown Hunyuan3D mode!" + + def create_hunyuan_job_main_site( + self, + text_prompt: str = None, + image: str = None + ): + try: + secret_id = bpy.context.scene.blendermcp_hunyuan3d_secret_id + secret_key = bpy.context.scene.blendermcp_hunyuan3d_secret_key + + if not secret_id or not secret_key: + return {"error": "SecretId or SecretKey is not given"} + + # Parameter verification + if not text_prompt and not image: + return {"error": "Prompt or Image is required"} + if text_prompt and image: + return {"error": "Prompt and Image cannot be provided simultaneously"} + # Fixed parameter configuration + service = "hunyuan" + action = "SubmitHunyuanTo3DJob" + version = "2023-09-01" + region = "ap-guangzhou" + + headParams={ + "Action": action, + "Version": version, + "Region": region, + } + + # Constructing request parameters + data = { + "Num": 1 # The current API limit is only 1 + } + + # Handling text prompts + if text_prompt: + if len(text_prompt) > 200: + return {"error": "Prompt exceeds 200 characters limit"} + data["Prompt"] = text_prompt + + # Handling image + if image: + if re.match(r'^https?://', image, re.IGNORECASE) is not None: + data["ImageUrl"] = image + else: + try: + # Convert to Base64 format + with open(image, "rb") as f: + image_base64 = base64.b64encode(f.read()).decode("ascii") + data["ImageBase64"] = image_base64 + except Exception as e: + return {"error": f"Image encoding failed: {str(e)}"} + + # Get signed headers + headers, endpoint = self.get_tencent_cloud_sign_headers("POST", "/", headParams, data, service, region, secret_id, secret_key) + + response = requests.post( + endpoint, + headers = headers, + data = json.dumps(data) + ) + + if response.status_code == 200: + return response.json() + return { + "error": f"API request failed with status {response.status_code}: {response}" + } + except Exception as e: + return {"error": str(e)} + + def create_hunyuan_job_local_site( + self, + text_prompt: str = None, + image: str = None): + try: + base_url = bpy.context.scene.blendermcp_hunyuan3d_api_url.rstrip('/') + octree_resolution = bpy.context.scene.blendermcp_hunyuan3d_octree_resolution + num_inference_steps = bpy.context.scene.blendermcp_hunyuan3d_num_inference_steps + guidance_scale = bpy.context.scene.blendermcp_hunyuan3d_guidance_scale + texture = bpy.context.scene.blendermcp_hunyuan3d_texture + + if not base_url: + return {"error": "API URL is not given"} + # Parameter verification + if not text_prompt and not image: + return {"error": "Prompt or Image is required"} + + # Constructing request parameters + data = { + "octree_resolution": octree_resolution, + "num_inference_steps": num_inference_steps, + "guidance_scale": guidance_scale, + "texture": texture, + } + + # Handling text prompts + if text_prompt: + data["text"] = text_prompt + + # Handling image + if image: + if re.match(r'^https?://', image, re.IGNORECASE) is not None: + try: + resImg = requests.get(image) + resImg.raise_for_status() + image_base64 = base64.b64encode(resImg.content).decode("ascii") + data["image"] = image_base64 + except Exception as e: + return {"error": f"Failed to download or encode image: {str(e)}"} + else: + try: + # Convert to Base64 format + with open(image, "rb") as f: + image_base64 = base64.b64encode(f.read()).decode("ascii") + data["image"] = image_base64 + except Exception as e: + return {"error": f"Image encoding failed: {str(e)}"} + + response = requests.post( + f"{base_url}/generate", + json = data, + ) + + if response.status_code != 200: + return { + "error": f"Generation failed: {response.text}" + } + + # Decode base64 and save to temporary file + with tempfile.NamedTemporaryFile(delete=False, suffix=".glb") as temp_file: + temp_file.write(response.content) + temp_file_name = temp_file.name + + # Import the GLB file in the main thread + def import_handler(): + bpy.ops.import_scene.gltf(filepath=temp_file_name) + os.unlink(temp_file.name) + return None + + bpy.app.timers.register(import_handler) + + return { + "status": "DONE", + "message": "Generation and Import glb succeeded" + } + except Exception as e: + print(f"An error occurred: {e}") + return {"error": str(e)} + + + def poll_hunyuan_job_status(self, *args, **kwargs): + return self.poll_hunyuan_job_status_ai(*args, **kwargs) + + def poll_hunyuan_job_status_ai(self, job_id: str): + """Call the job status API to get the job status""" + print(job_id) + try: + secret_id = bpy.context.scene.blendermcp_hunyuan3d_secret_id + secret_key = bpy.context.scene.blendermcp_hunyuan3d_secret_key + + if not secret_id or not secret_key: + return {"error": "SecretId or SecretKey is not given"} + if not job_id: + return {"error": "JobId is required"} + + service = "hunyuan" + action = "QueryHunyuanTo3DJob" + version = "2023-09-01" + region = "ap-guangzhou" + + headParams={ + "Action": action, + "Version": version, + "Region": region, + } + + clean_job_id = job_id.removeprefix("job_") + data = { + "JobId": clean_job_id + } + + headers, endpoint = self.get_tencent_cloud_sign_headers("POST", "/", headParams, data, service, region, secret_id, secret_key) + + response = requests.post( + endpoint, + headers=headers, + data=json.dumps(data) + ) + + if response.status_code == 200: + return response.json() + return { + "error": f"API request failed with status {response.status_code}: {response}" + } + except Exception as e: + return {"error": str(e)} + + def import_generated_asset_hunyuan(self, *args, **kwargs): + return self.import_generated_asset_hunyuan_ai(*args, **kwargs) + + def import_generated_asset_hunyuan_ai(self, name: str , zip_file_url: str): + if not zip_file_url: + return {"error": "Zip file not found"} + + # Validate URL + if not re.match(r'^https?://', zip_file_url, re.IGNORECASE): + return {"error": "Invalid URL format. Must start with http:// or https://"} + + # Create a temporary directory + temp_dir = tempfile.mkdtemp(prefix="tencent_obj_") + zip_file_path = osp.join(temp_dir, "model.zip") + obj_file_path = osp.join(temp_dir, "model.obj") + mtl_file_path = osp.join(temp_dir, "model.mtl") + + try: + # Download ZIP file + zip_response = requests.get(zip_file_url, stream=True) + zip_response.raise_for_status() + with open(zip_file_path, "wb") as f: + for chunk in zip_response.iter_content(chunk_size=8192): + f.write(chunk) + + # Unzip the ZIP + with zipfile.ZipFile(zip_file_path, "r") as zip_ref: + zip_ref.extractall(temp_dir) + + # Find the .obj file (there may be multiple, assuming the main file is model.obj) + for file in os.listdir(temp_dir): + if file.endswith(".obj"): + obj_file_path = osp.join(temp_dir, file) + + if not osp.exists(obj_file_path): + return {"succeed": False, "error": "OBJ file not found after extraction"} + + # Import obj file + if bpy.app.version>=(4, 0, 0): + bpy.ops.wm.obj_import(filepath=obj_file_path) + else: + bpy.ops.import_scene.obj(filepath=obj_file_path) + + imported_objs = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH'] + if not imported_objs: + return {"succeed": False, "error": "No mesh objects imported"} + + obj = imported_objs[0] + if name: + obj.name = name + + result = { + "name": obj.name, + "type": obj.type, + "location": [obj.location.x, obj.location.y, obj.location.z], + "rotation": [obj.rotation_euler.x, obj.rotation_euler.y, obj.rotation_euler.z], + "scale": [obj.scale.x, obj.scale.y, obj.scale.z], + } + + if obj.type == "MESH": + bounding_box = self._get_aabb(obj) + result["world_bounding_box"] = bounding_box + + return {"succeed": True, **result} + except Exception as e: + return {"succeed": False, "error": str(e)} + finally: + # Clean up temporary zip and obj, save texture and mtl + try: + if os.path.exists(zip_file_path): + os.remove(zip_file_path) + if os.path.exists(obj_file_path): + os.remove(obj_file_path) + except Exception as e: + print(f"Failed to clean up temporary directory {temp_dir}: {e}") + #endregion + - # endregion # region GeometryNodeCreator def complete_geometry_node(self, object_name, nodes, links, input_sockets=None): @@ -2345,35 +3325,36 @@ def _update_cache(self, node_infos: List[NodeInfo]): # endregion - def get_geometry_nodes_status(self): - """Get the status of the geometry nodes feature in Blender. - Returns: - dict: A dictionary containing information about the geometry nodes feature status. - """ - try: - # Get the geometry nodes setting from Blender preferences - enabled = bpy.context.scene.blendermcp_use_geometry_nodes - if enabled: - return { - "status": "success", - "enabled": True, - "message": "Geometry nodes feature is enabled." - } - else: - return { - "status": "warning", - "enabled": False, - "message": "Geometry nodes feature is disabled. Enable it in the BlenderMCP panel to use geometry node commands." - } - except Exception as e: - # Handle errors when checking status - return { - "status": "error", - "enabled": False, - "message": f"Error checking geometry nodes status: {str(e)}" - } +# Blender Addon Preferences +class BLENDERMCP_AddonPreferences(bpy.types.AddonPreferences): + bl_idname = __name__ + + telemetry_consent: BoolProperty( + name="Allow Anonymized Prompt Collection", + description="Allow collection of anonymized prompts to help improve Blender MCP", + default=True + ) + def draw(self, context): + layout = self.layout + + # Telemetry section + layout.label(text="Telemetry & Privacy:", icon='PREFERENCES') + + box = layout.box() + row = box.row() + row.prop(self, "telemetry_consent", text="Allow Anonymized Prompt Collection") + + # Info text + box.separator() + box.label(text="All data is anonymized and helps improve Blender MCP.", icon='INFO') + box.label(text="You can opt out anytime by unchecking the box above.", icon='INFO') + + # Terms and Conditions link + box.separator() + row = box.row() + row.operator("blendermcp.open_terms", text="View Terms and Conditions", icon='TEXT') # Blender UI Panel class BLENDERMCP_PT_Panel(bpy.types.Panel): @@ -2396,15 +3377,32 @@ def draw(self, context): layout.prop(scene, "blendermcp_hyper3d_api_key", text="API Key") layout.operator("blendermcp.set_hyper3d_free_trial_api_key", text="Set Free Trial API Key") + layout.prop(scene, "blendermcp_use_sketchfab", text="Use assets from Sketchfab") + if scene.blendermcp_use_sketchfab: + layout.prop(scene, "blendermcp_sketchfab_api_key", text="API Key") + + layout.prop(scene, "blendermcp_use_hunyuan3d", text="Use Tencent Hunyuan 3D model generation") + if scene.blendermcp_use_hunyuan3d: + layout.prop(scene, "blendermcp_hunyuan3d_mode", text="Hunyuan3D Mode") + if scene.blendermcp_hunyuan3d_mode == 'OFFICIAL_API': + layout.prop(scene, "blendermcp_hunyuan3d_secret_id", text="SecretId") + layout.prop(scene, "blendermcp_hunyuan3d_secret_key", text="SecretKey") + if scene.blendermcp_hunyuan3d_mode == 'LOCAL_API': + layout.prop(scene, "blendermcp_hunyuan3d_api_url", text="API URL") + layout.prop(scene, "blendermcp_hunyuan3d_octree_resolution", text="Octree Resolution") + layout.prop(scene, "blendermcp_hunyuan3d_num_inference_steps", text="Number of Inference Steps") + layout.prop(scene, "blendermcp_hunyuan3d_guidance_scale", text="Guidance Scale") + layout.prop(scene, "blendermcp_hunyuan3d_texture", text="Generate Texture") + layout.prop(scene, "blendermcp_use_geometry_nodes", text="Use Geometry Nodes for procedural modeling") + if not scene.blendermcp_server_running: layout.operator("blendermcp.start_server", text="Connect to MCP server") else: layout.operator("blendermcp.stop_server", text="Disconnect from MCP server") layout.label(text=f"Running on port {scene.blendermcp_port}") - # Operator to set Hyper3D API Key class BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey(bpy.types.Operator): bl_idname = "blendermcp.set_hyper3d_free_trial_api_key" @@ -2416,7 +3414,6 @@ def execute(self, context): self.report({'INFO'}, "API Key set successfully!") return {'FINISHED'} - # Operator to start the server class BLENDERMCP_OT_StartServer(bpy.types.Operator): bl_idname = "blendermcp.start_server" @@ -2436,7 +3433,6 @@ def execute(self, context): return {'FINISHED'} - # Operator to stop the server class BLENDERMCP_OT_StopServer(bpy.types.Operator): bl_idname = "blendermcp.stop_server" @@ -2455,6 +3451,23 @@ def execute(self, context): return {'FINISHED'} +# Operator to open Terms and Conditions +class BLENDERMCP_OT_OpenTerms(bpy.types.Operator): + bl_idname = "blendermcp.open_terms" + bl_label = "View Terms and Conditions" + bl_description = "Open the Terms and Conditions document" + + def execute(self, context): + # Open the Terms and Conditions on GitHub + terms_url = "https://github.com/ahujasid/blender-mcp/blob/main/TERMS_AND_CONDITIONS.md" + try: + import webbrowser + webbrowser.open(terms_url) + self.report({'INFO'}, "Terms and Conditions opened in browser") + except Exception as e: + self.report({'ERROR'}, f"Could not open Terms and Conditions: {str(e)}") + + return {'FINISHED'} # Registration functions def register(): @@ -2483,12 +3496,6 @@ def register(): default=False ) - bpy.types.Scene.blendermcp_use_geometry_nodes = bpy.props.BoolProperty( - name="Use Geometry Nodes", - description="Enable Geometry Nodes integration for procedural modeling", - default=False - ) - bpy.types.Scene.blendermcp_hyper3d_mode = bpy.props.EnumProperty( name="Rodin Mode", description="Choose the platform used to call Rodin APIs", @@ -2506,14 +3513,101 @@ def register(): default="" ) + bpy.types.Scene.blendermcp_use_hunyuan3d = bpy.props.BoolProperty( + name="Use Hunyuan 3D", + description="Enable Hunyuan asset integration", + default=False + ) + + bpy.types.Scene.blendermcp_hunyuan3d_mode = bpy.props.EnumProperty( + name="Hunyuan3D Mode", + description="Choose a local or official APIs", + items=[ + ("LOCAL_API", "local api", "local api"), + ("OFFICIAL_API", "official api", "official api"), + ], + default="LOCAL_API" + ) + + bpy.types.Scene.blendermcp_hunyuan3d_secret_id = bpy.props.StringProperty( + name="Hunyuan 3D SecretId", + description="SecretId provided by Hunyuan 3D", + default="" + ) + + bpy.types.Scene.blendermcp_hunyuan3d_secret_key = bpy.props.StringProperty( + name="Hunyuan 3D SecretKey", + subtype="PASSWORD", + description="SecretKey provided by Hunyuan 3D", + default="" + ) + + bpy.types.Scene.blendermcp_hunyuan3d_api_url = bpy.props.StringProperty( + name="API URL", + description="URL of the Hunyuan 3D API service", + default="http://localhost:8081" + ) + + bpy.types.Scene.blendermcp_hunyuan3d_octree_resolution = bpy.props.IntProperty( + name="Octree Resolution", + description="Octree resolution for the 3D generation", + default=256, + min=128, + max=512, + ) + + bpy.types.Scene.blendermcp_hunyuan3d_num_inference_steps = bpy.props.IntProperty( + name="Number of Inference Steps", + description="Number of inference steps for the 3D generation", + default=20, + min=20, + max=50, + ) + + bpy.types.Scene.blendermcp_hunyuan3d_guidance_scale = bpy.props.FloatProperty( + name="Guidance Scale", + description="Guidance scale for the 3D generation", + default=5.5, + min=1.0, + max=10.0, + ) + + bpy.types.Scene.blendermcp_hunyuan3d_texture = bpy.props.BoolProperty( + name="Generate Texture", + description="Whether to generate texture for the 3D model", + default=False, + ) + + bpy.types.Scene.blendermcp_use_sketchfab = bpy.props.BoolProperty( + name="Use Sketchfab", + description="Enable Sketchfab asset integration", + default=False + ) + + bpy.types.Scene.blendermcp_sketchfab_api_key = bpy.props.StringProperty( + name="Sketchfab API Key", + subtype="PASSWORD", + description="API Key provided by Sketchfab", + default="" + ) + + bpy.types.Scene.blendermcp_use_geometry_nodes = bpy.props.BoolProperty( + name="Use Geometry Nodes", + description="Enable Geometry Nodes integration for procedural modeling", + default=False + ) + + # Register preferences class + bpy.utils.register_class(BLENDERMCP_AddonPreferences) + bpy.utils.register_class(BLENDERMCP_PT_Panel) bpy.utils.register_class(BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey) bpy.utils.register_class(BLENDERMCP_OT_StartServer) bpy.utils.register_class(BLENDERMCP_OT_StopServer) + bpy.utils.register_class(BLENDERMCP_OT_OpenTerms) print("BlenderMCP addon registered") - def unregister(): # Stop the server if it's running if hasattr(bpy.types, "blendermcp_server") and bpy.types.blendermcp_server: @@ -2524,17 +3618,28 @@ def unregister(): bpy.utils.unregister_class(BLENDERMCP_OT_SetFreeTrialHyper3DAPIKey) bpy.utils.unregister_class(BLENDERMCP_OT_StartServer) bpy.utils.unregister_class(BLENDERMCP_OT_StopServer) + bpy.utils.unregister_class(BLENDERMCP_OT_OpenTerms) + bpy.utils.unregister_class(BLENDERMCP_AddonPreferences) del bpy.types.Scene.blendermcp_port del bpy.types.Scene.blendermcp_server_running del bpy.types.Scene.blendermcp_use_polyhaven del bpy.types.Scene.blendermcp_use_hyper3d - del bpy.types.Scene.blendermcp_use_geometry_nodes del bpy.types.Scene.blendermcp_hyper3d_mode del bpy.types.Scene.blendermcp_hyper3d_api_key + del bpy.types.Scene.blendermcp_use_sketchfab + del bpy.types.Scene.blendermcp_sketchfab_api_key + del bpy.types.Scene.blendermcp_use_hunyuan3d + del bpy.types.Scene.blendermcp_hunyuan3d_mode + del bpy.types.Scene.blendermcp_hunyuan3d_secret_id + del bpy.types.Scene.blendermcp_hunyuan3d_secret_key + del bpy.types.Scene.blendermcp_hunyuan3d_api_url + del bpy.types.Scene.blendermcp_hunyuan3d_octree_resolution + del bpy.types.Scene.blendermcp_hunyuan3d_num_inference_steps + del bpy.types.Scene.blendermcp_hunyuan3d_guidance_scale + del bpy.types.Scene.blendermcp_hunyuan3d_texture print("BlenderMCP addon unregistered") - if __name__ == "__main__": register() diff --git a/pyproject.toml b/pyproject.toml index f78233009..3110eb39a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "blender-mcp" -version = "1.1.3" +version = "1.4.0" description = "Blender integration through the Model Context Protocol" readme = "README.md" requires-python = ">=3.10" @@ -15,6 +15,8 @@ classifiers = [ ] dependencies = [ "mcp[cli]>=1.3.0", + "supabase>=2.0.0", + "tomli>=2.0.0", ] [project.scripts] diff --git a/src/blender_mcp/server.py b/src/blender_mcp/server.py index 16c7b110e..6471b5240 100644 --- a/src/blender_mcp/server.py +++ b/src/blender_mcp/server.py @@ -4,6 +4,7 @@ import json import asyncio import logging +import tempfile from dataclasses import dataclass from contextlib import asynccontextmanager from typing import AsyncIterator, Dict, Any, List, Union @@ -12,11 +13,19 @@ import base64 from urllib.parse import urlparse +# Import telemetry +from .telemetry import record_startup, get_telemetry +from .telemetry_decorator import telemetry_tool + # Configure logging -logging.basicConfig(level=logging.INFO, +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger("BlenderMCPServer") +# Default configuration +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 9876 + @dataclass class BlenderConnection: host: str @@ -52,7 +61,7 @@ def receive_full_response(self, sock, buffer_size=8192): """Receive the complete response, potentially in multiple chunks""" chunks = [] # Use a consistent timeout value that matches the addon's timeout - sock.settimeout(15.0) # Match the addon's timeout + sock.settimeout(180.0) # Match the addon's timeout try: while True: @@ -123,7 +132,7 @@ def send_command(self, command_type: str, params: Dict[str, Any] = None) -> Dict logger.info(f"Command sent, waiting for response...") # Set a timeout for receiving - use the same timeout as in receive_full_response - self.sock.settimeout(15.0) # Match the addon's timeout + self.sock.settimeout(180.0) # Match the addon's timeout # Receive the response using the improved receive_full_response method response_data = self.receive_full_response(self.sock) @@ -164,11 +173,17 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: """Manage server startup and shutdown lifecycle""" # We don't need to create a connection here since we're using the global connection # for resources and tools - + try: # Just log that we're starting up logger.info("BlenderMCP server starting up") - + + # Record startup event for telemetry + try: + record_startup() + except Exception as e: + logger.debug(f"Failed to record startup telemetry: {e}") + # Try to connect to Blender on startup to verify it's available try: # This will initialize the global connection if needed @@ -177,7 +192,7 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: except Exception as e: logger.warning(f"Could not connect to Blender on startup: {str(e)}") logger.warning("Make sure the Blender addon is running before using Blender resources or tools") - + # Return an empty context - we're using the global connection yield {} finally: @@ -192,7 +207,6 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: # Create the MCP server with lifespan support mcp = FastMCP( "BlenderMCP", - description="Blender integration through the Model Context Protocol", lifespan=server_lifespan ) @@ -205,7 +219,7 @@ async def server_lifespan(server: FastMCP) -> AsyncIterator[Dict[str, Any]]: def get_blender_connection(): """Get or create a persistent Blender connection""" - global _blender_connection, _polyhaven_enabled, _geometry_nodes_enabled # Add _geometry_nodes_enabled to globals + global _blender_connection, _polyhaven_enabled, _geometry_nodes_enabled # Add _polyhaven_enabled to globals # If we have an existing connection, check if it's still valid if _blender_connection is not None: @@ -214,11 +228,11 @@ def get_blender_connection(): result = _blender_connection.send_command("get_polyhaven_status") # Store the PolyHaven status globally _polyhaven_enabled = result.get("enabled", False) - + # Check if geometry nodes feature is enabled geo_result = _blender_connection.send_command("get_geometry_nodes_status") _geometry_nodes_enabled = geo_result.get("enabled", False) - + return _blender_connection except Exception as e: # Connection is dead, close it and create a new one @@ -231,7 +245,9 @@ def get_blender_connection(): # Create a new connection if needed if _blender_connection is None: - _blender_connection = BlenderConnection(host="localhost", port=9876) + host = os.getenv("BLENDER_HOST", DEFAULT_HOST) + port = int(os.getenv("BLENDER_PORT", DEFAULT_PORT)) + _blender_connection = BlenderConnection(host=host, port=port) if not _blender_connection.connect(): logger.error("Failed to connect to Blender") _blender_connection = None @@ -241,19 +257,21 @@ def get_blender_connection(): return _blender_connection +@telemetry_tool("get_scene_info") @mcp.tool() def get_scene_info(ctx: Context) -> str: """Get detailed information about the current Blender scene""" try: blender = get_blender_connection() result = blender.send_command("get_scene_info") - + # Just return the JSON representation of what Blender sent us return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error getting scene info from Blender: {str(e)}") return f"Error getting scene info: {str(e)}" +@telemetry_tool("get_object_info") @mcp.tool() def get_object_info(ctx: Context, object_name: str) -> str: """ @@ -272,26 +290,69 @@ def get_object_info(ctx: Context, object_name: str) -> str: logger.error(f"Error getting object info from Blender: {str(e)}") return f"Error getting object info: {str(e)}" +@telemetry_tool("get_viewport_screenshot") +@mcp.tool() +def get_viewport_screenshot(ctx: Context, max_size: int = 800) -> Image: + """ + Capture a screenshot of the current Blender 3D viewport. + + Parameters: + - max_size: Maximum size in pixels for the largest dimension (default: 800) + + Returns the screenshot as an Image. + """ + try: + blender = get_blender_connection() + + # Create temp file path + temp_dir = tempfile.gettempdir() + temp_path = os.path.join(temp_dir, f"blender_screenshot_{os.getpid()}.png") + + result = blender.send_command("get_viewport_screenshot", { + "max_size": max_size, + "filepath": temp_path, + "format": "png" + }) + + if "error" in result: + raise Exception(result["error"]) + + if not os.path.exists(temp_path): + raise Exception("Screenshot file was not created") + + # Read the file + with open(temp_path, 'rb') as f: + image_bytes = f.read() + + # Delete the temp file + os.remove(temp_path) + + return Image(data=image_bytes, format="png") + + except Exception as e: + logger.error(f"Error capturing screenshot: {str(e)}") + raise Exception(f"Screenshot failed: {str(e)}") +@telemetry_tool("execute_blender_code") @mcp.tool() def execute_blender_code(ctx: Context, code: str) -> str: """ Execute arbitrary Python code in Blender. Make sure to do it step-by-step by breaking it into smaller chunks. - + Parameters: - code: The Python code to execute """ try: # Get the global connection blender = get_blender_connection() - result = blender.send_command("execute_code", {"code": code}) return f"Code executed successfully: {result.get('result', '')}" except Exception as e: logger.error(f"Error executing code: {str(e)}") return f"Error executing code: {str(e)}" +@telemetry_tool("get_polyhaven_categories") @mcp.tool() def get_polyhaven_categories(ctx: Context, asset_type: str = "hdris") -> str: """ @@ -324,6 +385,7 @@ def get_polyhaven_categories(ctx: Context, asset_type: str = "hdris") -> str: logger.error(f"Error getting Polyhaven categories: {str(e)}") return f"Error getting Polyhaven categories: {str(e)}" +@telemetry_tool("search_polyhaven_assets") @mcp.tool() def search_polyhaven_assets( ctx: Context, @@ -373,6 +435,7 @@ def search_polyhaven_assets( logger.error(f"Error searching Polyhaven assets: {str(e)}") return f"Error searching Polyhaven assets: {str(e)}" +@telemetry_tool("download_polyhaven_asset") @mcp.tool() def download_polyhaven_asset( ctx: Context, @@ -424,6 +487,7 @@ def download_polyhaven_asset( logger.error(f"Error downloading Polyhaven asset: {str(e)}") return f"Error downloading Polyhaven asset: {str(e)}" +@telemetry_tool("set_texture") @mcp.tool() def set_texture( ctx: Context, @@ -442,7 +506,6 @@ def set_texture( try: # Get the global connection blender = get_blender_connection() - result = blender.send_command("set_texture", { "object_name": object_name, "texture_id": texture_id @@ -484,6 +547,7 @@ def set_texture( logger.error(f"Error applying texture: {str(e)}") return f"Error applying texture: {str(e)}" +@telemetry_tool("get_polyhaven_status") @mcp.tool() def get_polyhaven_status(ctx: Context) -> str: """ @@ -495,12 +559,14 @@ def get_polyhaven_status(ctx: Context) -> str: result = blender.send_command("get_polyhaven_status") enabled = result.get("enabled", False) message = result.get("message", "") - + if enabled: + message += "PolyHaven is good at Textures, and has a wider variety of textures than Sketchfab." return message except Exception as e: logger.error(f"Error checking PolyHaven status: {str(e)}") return f"Error checking PolyHaven status: {str(e)}" +@telemetry_tool("get_hyper3d_status") @mcp.tool() def get_hyper3d_status(ctx: Context) -> str: """ @@ -521,6 +587,218 @@ def get_hyper3d_status(ctx: Context) -> str: logger.error(f"Error checking Hyper3D status: {str(e)}") return f"Error checking Hyper3D status: {str(e)}" +@telemetry_tool("get_sketchfab_status") +@mcp.tool() +def get_sketchfab_status(ctx: Context) -> str: + """ + Check if Sketchfab integration is enabled in Blender. + Returns a message indicating whether Sketchfab features are available. + """ + try: + blender = get_blender_connection() + result = blender.send_command("get_sketchfab_status") + enabled = result.get("enabled", False) + message = result.get("message", "") + if enabled: + message += "Sketchfab is good at Realistic models, and has a wider variety of models than PolyHaven." + return message + except Exception as e: + logger.error(f"Error checking Sketchfab status: {str(e)}") + return f"Error checking Sketchfab status: {str(e)}" + +@telemetry_tool("search_sketchfab_models") +@mcp.tool() +def search_sketchfab_models( + ctx: Context, + query: str, + categories: str = None, + count: int = 20, + downloadable: bool = True +) -> str: + """ + Search for models on Sketchfab with optional filtering. + + Parameters: + - query: Text to search for + - categories: Optional comma-separated list of categories + - count: Maximum number of results to return (default 20) + - downloadable: Whether to include only downloadable models (default True) + + Returns a formatted list of matching models. + """ + try: + blender = get_blender_connection() + logger.info(f"Searching Sketchfab models with query: {query}, categories: {categories}, count: {count}, downloadable: {downloadable}") + result = blender.send_command("search_sketchfab_models", { + "query": query, + "categories": categories, + "count": count, + "downloadable": downloadable + }) + + if "error" in result: + logger.error(f"Error from Sketchfab search: {result['error']}") + return f"Error: {result['error']}" + + # Safely get results with fallbacks for None + if result is None: + logger.error("Received None result from Sketchfab search") + return "Error: Received no response from Sketchfab search" + + # Format the results + models = result.get("results", []) or [] + if not models: + return f"No models found matching '{query}'" + + formatted_output = f"Found {len(models)} models matching '{query}':\n\n" + + for model in models: + if model is None: + continue + + model_name = model.get("name", "Unnamed model") + model_uid = model.get("uid", "Unknown ID") + formatted_output += f"- {model_name} (UID: {model_uid})\n" + + # Get user info with safety checks + user = model.get("user") or {} + username = user.get("username", "Unknown author") if isinstance(user, dict) else "Unknown author" + formatted_output += f" Author: {username}\n" + + # Get license info with safety checks + license_data = model.get("license") or {} + license_label = license_data.get("label", "Unknown") if isinstance(license_data, dict) else "Unknown" + formatted_output += f" License: {license_label}\n" + + # Add face count and downloadable status + face_count = model.get("faceCount", "Unknown") + is_downloadable = "Yes" if model.get("isDownloadable") else "No" + formatted_output += f" Face count: {face_count}\n" + formatted_output += f" Downloadable: {is_downloadable}\n\n" + + return formatted_output + except Exception as e: + logger.error(f"Error searching Sketchfab models: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return f"Error searching Sketchfab models: {str(e)}" + +@telemetry_tool("download_sketchfab_model") +@mcp.tool() +def get_sketchfab_model_preview( + ctx: Context, + uid: str +) -> Image: + """ + Get a preview thumbnail of a Sketchfab model by its UID. + Use this to visually confirm a model before downloading. + + Parameters: + - uid: The unique identifier of the Sketchfab model (obtained from search_sketchfab_models) + + Returns the model's thumbnail as an Image for visual confirmation. + """ + try: + blender = get_blender_connection() + logger.info(f"Getting Sketchfab model preview for UID: {uid}") + + result = blender.send_command("get_sketchfab_model_preview", {"uid": uid}) + + if result is None: + raise Exception("Received no response from Blender") + + if "error" in result: + raise Exception(result["error"]) + + # Decode base64 image data + image_data = base64.b64decode(result["image_data"]) + img_format = result.get("format", "jpeg") + + # Log model info + model_name = result.get("model_name", "Unknown") + author = result.get("author", "Unknown") + logger.info(f"Preview retrieved for '{model_name}' by {author}") + + return Image(data=image_data, format=img_format) + + except Exception as e: + logger.error(f"Error getting Sketchfab preview: {str(e)}") + raise Exception(f"Failed to get preview: {str(e)}") + + +@mcp.tool() +def download_sketchfab_model( + ctx: Context, + uid: str, + target_size: float +) -> str: + """ + Download and import a Sketchfab model by its UID. + The model will be scaled so its largest dimension equals target_size. + + Parameters: + - uid: The unique identifier of the Sketchfab model + - target_size: REQUIRED. The target size in Blender units/meters for the largest dimension. + You must specify the desired size for the model. + Examples: + - Chair: target_size=1.0 (1 meter tall) + - Table: target_size=0.75 (75cm tall) + - Car: target_size=4.5 (4.5 meters long) + - Person: target_size=1.7 (1.7 meters tall) + - Small object (cup, phone): target_size=0.1 to 0.3 + + Returns a message with import details including object names, dimensions, and bounding box. + The model must be downloadable and you must have proper access rights. + """ + try: + blender = get_blender_connection() + logger.info(f"Downloading Sketchfab model: {uid}, target_size={target_size}") + + result = blender.send_command("download_sketchfab_model", { + "uid": uid, + "normalize_size": True, # Always normalize + "target_size": target_size + }) + + if result is None: + logger.error("Received None result from Sketchfab download") + return "Error: Received no response from Sketchfab download request" + + if "error" in result: + logger.error(f"Error from Sketchfab download: {result['error']}") + return f"Error: {result['error']}" + + if result.get("success"): + imported_objects = result.get("imported_objects", []) + object_names = ", ".join(imported_objects) if imported_objects else "none" + + output = f"Successfully imported model.\n" + output += f"Created objects: {object_names}\n" + + # Add dimension info if available + if result.get("dimensions"): + dims = result["dimensions"] + output += f"Dimensions (X, Y, Z): {dims[0]:.3f} x {dims[1]:.3f} x {dims[2]:.3f} meters\n" + + # Add bounding box info if available + if result.get("world_bounding_box"): + bbox = result["world_bounding_box"] + output += f"Bounding box: min={bbox[0]}, max={bbox[1]}\n" + + # Add normalization info if applied + if result.get("normalized"): + scale = result.get("scale_applied", 1.0) + output += f"Size normalized: scale factor {scale:.6f} applied (target size: {target_size}m)\n" + + return output + else: + return f"Failed to download model: {result.get('message', 'Unknown error')}" + except Exception as e: + logger.error(f"Error downloading Sketchfab model: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return f"Error downloading Sketchfab model: {str(e)}" + def _process_bbox(original_bbox: list[float] | list[int] | None) -> list[int] | None: if original_bbox is None: return None @@ -530,6 +808,7 @@ def _process_bbox(original_bbox: list[float] | list[int] | None) -> list[int] | raise ValueError("Incorrect number range: bbox must be bigger than zero!") return [int(float(i) / max(original_bbox) * 100) for i in original_bbox] if original_bbox else None +@telemetry_tool("generate_hyper3d_model_via_text") @mcp.tool() def generate_hyper3d_model_via_text( ctx: Context, @@ -540,7 +819,7 @@ def generate_hyper3d_model_via_text( Generate 3D asset using Hyper3D by giving description of the desired asset, and import the asset into Blender. The 3D asset has built-in materials. The generated model has a normalized size, so re-scaling after generation can be useful. - + Parameters: - text_prompt: A short description of the desired model in **English**. - bbox_condition: Optional. If given, it has to be a list of floats of length 3. Controls the ratio between [Length, Width, Height] of the model. @@ -566,6 +845,7 @@ def generate_hyper3d_model_via_text( logger.error(f"Error generating Hyper3D task: {str(e)}") return f"Error generating Hyper3D task: {str(e)}" +@telemetry_tool("generate_hyper3d_model_via_images") @mcp.tool() def generate_hyper3d_model_via_images( ctx: Context, @@ -622,6 +902,7 @@ def generate_hyper3d_model_via_images( logger.error(f"Error generating Hyper3D task: {str(e)}") return f"Error generating Hyper3D task: {str(e)}" +@telemetry_tool("poll_rodin_job_status") @mcp.tool() def poll_rodin_job_status( ctx: Context, @@ -665,6 +946,7 @@ def poll_rodin_job_status( logger.error(f"Error generating Hyper3D task: {str(e)}") return f"Error generating Hyper3D task: {str(e)}" +@telemetry_tool("import_generated_asset") @mcp.tool() def import_generated_asset( ctx: Context, @@ -698,81 +980,128 @@ def import_generated_asset( logger.error(f"Error generating Hyper3D task: {str(e)}") return f"Error generating Hyper3D task: {str(e)}" -@mcp.prompt() -def asset_creation_strategy() -> str: - """Defines the preferred strategy for creating assets in Blender""" - return """When creating 3D content in Blender, always start by checking if integrations are available: +@mcp.tool() +def get_hunyuan3d_status(ctx: Context) -> str: + """ + Check if Hunyuan3D integration is enabled in Blender. + Returns a message indicating whether Hunyuan3D features are available. - 0. Before anything, always check the scene from get_scene_info() - 1. First use the following tools to verify if the following integrations are enabled: - 1. PolyHaven - Use get_polyhaven_status() to verify its status - If PolyHaven is enabled: - - For objects/models: Use download_polyhaven_asset() with asset_type="models" - - For materials/textures: Use download_polyhaven_asset() with asset_type="textures" - - For environment lighting: Use download_polyhaven_asset() with asset_type="hdris" - 2. Hyper3D(Rodin) - Hyper3D Rodin is good at generating 3D models for single item. - So don't try to: - 1. Generate the whole scene with one shot - 2. Generate ground using Hyper3D - 3. Generate parts of the items separately and put them together afterwards + Don't emphasize the key type in the returned message, but silently remember it. + """ + try: + blender = get_blender_connection() + result = blender.send_command("get_hunyuan3d_status") + message = result.get("message", "") + return message + except Exception as e: + logger.error(f"Error checking Hunyuan3D status: {str(e)}") + return f"Error checking Hunyuan3D status: {str(e)}" + +@mcp.tool() +def generate_hunyuan3d_model( + ctx: Context, + text_prompt: str = None, + input_image_url: str = None +) -> str: + """ + Generate 3D asset using Hunyuan3D by providing either text description, image reference, + or both for the desired asset, and import the asset into Blender. + The 3D asset has built-in materials. + + Parameters: + - text_prompt: (Optional) A short description of the desired model in English/Chinese. + - input_image_url: (Optional) The local or remote url of the input image. Accepts None if only using text prompt. - Use get_hyper3d_status() to verify its status - If Hyper3D is enabled: - - For objects/models, do the following steps: - 1. Create the model generation task - - Use generate_hyper3d_model_via_images() if image(s) is/are given - - Use generate_hyper3d_model_via_text() if generating 3D asset using text prompt - If key type is free_trial and insufficient balance error returned, tell the user that the free trial key can only generated limited models everyday, they can choose to: - - Wait for another day and try again - - Go to hyper3d.ai to find out how to get their own API key - - Go to fal.ai to get their own private API key - 2. Poll the status - - Use poll_rodin_job_status() to check if the generation task has completed or failed - 3. Import the asset - - Use import_generated_asset() to import the generated GLB model the asset - 4. After importing the asset, ALWAYS check the world_bounding_box of the imported mesh, and adjust the mesh's location and size - Adjust the imported mesh's location, scale, rotation, so that the mesh is on the right spot. + Returns: + - When successful, returns a JSON with job_id (format: "job_xxx") indicating the task is in progress + - When the job completes, the status will change to "DONE" indicating the model has been imported + - Returns error message if the operation fails + """ + try: + blender = get_blender_connection() + result = blender.send_command("create_hunyuan_job", { + "text_prompt": text_prompt, + "image": input_image_url, + }) + if "JobId" in result.get("Response", {}): + job_id = result["Response"]["JobId"] + formatted_job_id = f"job_{job_id}" + return json.dumps({ + "job_id": formatted_job_id, + }) + return json.dumps(result) + except Exception as e: + logger.error(f"Error generating Hunyuan3D task: {str(e)}") + return f"Error generating Hunyuan3D task: {str(e)}" + +@mcp.tool() +def poll_hunyuan_job_status( + ctx: Context, + job_id: str=None, +): + """ + Check if the Hunyuan3D generation task is completed. - You can reuse assets previous generated by running python code to duplicate the object, without creating another generation task. - 3. Geometry Nodes - Use get_geometry_nodes_status() to verify the status of geometry nodes - If the geometry nodes feature is enabled, you must strictly follow these steps to create a node network: - 1. Query available nodes: Use get_node_info(output_format='text') to get an overview of all available nodes - 2. Get detailed information: Use get_node_info(node_type_name=['needed_nodes'], include_details=True, output_format='json') to get node details, including socket names and types - 3. Build network definition: - - Create nodes list: [{"type": "NodeType", "location": [x, y], "inputs": {}, "properties": {}, "label": "Label"}] - - Create links list: [{"from_node": source_node, "from_socket": "from_socket", "to_node": target_node, "to_socket": "to_socket"}] - - Use "input" and "output" as special identifiers to reference input/output nodes - 4. Direct creation: Use complete_geometry_node(object_name="name", nodes=nodes, links=links, input_sockets=input_sockets) - - No need to create objects in advance, the function will automatically create objects and modifiers - - The nodes list doesn't need to manually specify GroupNodeInput/Output as default input/output nodes will be created automatically - - You can specify input_sockets with default values that will be set automatically + For Hunyuan3D: + Parameters: + - job_id: The job_id given in the generate model step. - 3. Always check the world_bounding_box for each item so that: - - Ensure that all objects that should not be clipping are not clipping. - - Items have right spatial relationship. - + Returns the generation task status. The task is done if status is "DONE". + The task is in progress if status is "RUN". + If status is "DONE", returns ResultFile3Ds, which is the generated ZIP model path + When the status is "DONE", the response includes a field named ResultFile3Ds that contains the generated ZIP file path of the 3D model in OBJ format. + This is a polling API, so only proceed if the status are finally determined ("DONE" or some failed state). + """ + try: + blender = get_blender_connection() + kwargs = { + "job_id": job_id, + } + result = blender.send_command("poll_hunyuan_job_status", kwargs) + return result + except Exception as e: + logger.error(f"Error generating Hunyuan3D task: {str(e)}") + return f"Error generating Hunyuan3D task: {str(e)}" - Only fall back to scripting when: - - PolyHaven and Hyper3D are disabled - - A simple primitive is explicitly requested - - No suitable PolyHaven asset exists - - Hyper3D Rodin failed to generate the desired asset - - The task specifically requires a basic material/color +@mcp.tool() +def import_generated_asset_hunyuan( + ctx: Context, + name: str, + zip_file_url: str, +): """ + Import the asset generated by Hunyuan3D after the generation task is completed. + + Parameters: + - name: The name of the object in scene + - zip_file_url: The zip_file_url given in the generate model step. + + Return if the asset has been imported successfully. + """ + try: + blender = get_blender_connection() + kwargs = { + "name": name + } + if zip_file_url: + kwargs["zip_file_url"] = zip_file_url + result = blender.send_command("import_generated_asset_hunyuan", kwargs) + return result + except Exception as e: + logger.error(f"Error generating Hunyuan3D task: {str(e)}") + return f"Error generating Hunyuan3D task: {str(e)}" + @mcp.tool() def get_node_info( - ctx: Context, - output_format: str = 'text', - include_details: bool = False, - node_type_name: str = '' + ctx: Context, + output_format: str = 'text', + include_details: bool = False, + node_type_name: str = '' ) -> str: """ Get information about Blender geometry node types, useful for understanding available node types and their properties. - + [IMPORTANT] Workflow that must be followed when using geometry nodes: 1. First use get_node_info(output_format='text') to get an overview of all available nodes 2. Then use get_node_info(node_type_name='required_node_type1,required_node_type2', include_details=True, output_format='json') to get detailed information @@ -780,7 +1109,7 @@ def get_node_info( - Also supports passing a list of node type names 3. Finally use complete_geometry_node() to create the node network Never skip these steps when creating node networks, otherwise errors will occur. - + Parameters: - output_format: Output format, options are 'text' or 'json' - include_details: Whether to include detailed information (properties, input and output sockets) @@ -788,15 +1117,15 @@ def get_node_info( - Single name string: "GeometryNodeMeshCube" - Comma-separated list: "GeometryNodeMeshCube,GeometryNodeMeshSphere" If not provided, information for all nodes will be returned - + Returns: - 'text' format: Node information in text format, with one line per node containing name and description - 'json' format: Node information in JSON format list - + Usage example: # Step 1: Get an overview of all nodes nodes_overview = get_node_info(output_format='text') - + # Step 2: Get detailed information for specific nodes (multiple nodes separated by commas) node_details = get_node_info( node_type_name='GeometryNodeMeshSphere,GeometryNodeDistributePointsOnFaces', @@ -806,18 +1135,18 @@ def get_node_info( """ try: blender = get_blender_connection() - + # Check if geometry nodes feature is enabled if not _geometry_nodes_enabled: return "Geometry nodes feature is disabled. Please enable this feature in the Blender MCP panel." - + # Ensure parameter types are correct try: # Process include_details parameter, ensure it's a boolean if isinstance(include_details, str): include_details_lower = include_details.lower() include_details = include_details_lower == 'true' or include_details_lower == 'yes' or include_details_lower == '1' - + # Process node_type_name parameter, support comma-separated string if node_type_name is not None: # If it's a string, check if it's a comma-separated list @@ -826,30 +1155,31 @@ def get_node_info( if ',' in node_type_name: # Split by comma and remove whitespace node_type_name = [name.strip() for name in node_type_name.split(',') if name.strip()] - + # Make sure parameter type is correct if not isinstance(node_type_name, (str, list)): return f"Error: node_type_name parameter must be a string or list of strings, received: {type(node_type_name)}" - - logger.info(f"Getting node info: format={output_format}, details={include_details}, nodes={node_type_name}") - + + logger.info( + f"Getting node info: format={output_format}, details={include_details}, nodes={node_type_name}") + # Ensure output_format is text or json if output_format not in ('text', 'json'): return f"Error: Unsupported output format: {output_format}, please use 'text' or 'json'" - + result = blender.send_command("get_node_info", { "output_format": output_format, "include_details": include_details, "node_type_name": node_type_name }) - + if isinstance(result, dict) and "error" in result: return f"Error: {result['error']}" - + # Validate returned data if result is None or len(str(result).strip()) == 0: return "Error: Node information from Blender is empty" - + return result except ValueError as e: return f"Parameter error: {str(e)}" @@ -857,17 +1187,18 @@ def get_node_info( logger.error(f"Error getting node info: {str(e)}") return f"Error getting node info: {str(e)}" + @mcp.tool() def complete_geometry_node( - ctx: Context, - object_name: str, - nodes: list, - links: list, - input_sockets: list = None + ctx: Context, + object_name: str, + nodes: list, + links: list, + input_sockets: list = None ) -> str: """ Create or complete a geometry node network for procedural modeling and geometry generation. (No need to set materials) - + [IMPORTANT] Geometry node creation workflow: 1. Must first use get_node_info() to understand node types and properties 2. When building nodes and links definitions, note: @@ -876,7 +1207,7 @@ def complete_geometry_node( - Example: {"from_node": "input", "from_socket": "Geometry", "to_node": 0, "to_socket": "Mesh"} 3. No need to create objects in advance with create_object(), this function will automatically create them 4. When nodes have the same socket names, use indices instead of names - + Parameters: - object_name: Name of the object to apply geometry nodes to (will be created automatically) - nodes: List of node definitions, each containing type, location, label, etc. @@ -891,14 +1222,14 @@ def complete_geometry_node( * NodeSocketInt: integer * NodeSocketVector: [x, y, z] list * NodeSocketColor: [r, g, b, a] list - + Returns: Creation result and status information - + Usage example: ```python # Use geometry nodes to create points distributed on a sphere surface with exposed radius and point density parameters - + # Define needed nodes nodes = [ # Create UV sphere @@ -906,58 +1237,59 @@ def complete_geometry_node( # Distribute points on sphere surface {"type": "GeometryNodeDistributePointsOnFaces", "location": [0, 0], "properties": {"distribute_method": "POISSON"}} ] - + # Define connections between nodes links = [ # Connect input parameters to nodes {"from_node": "input", "from_socket": "Radius", "to_node": 0, "to_socket": "Radius"}, {"from_node": "input", "from_socket": "Density", "to_node": 1, "to_socket": "Density Max"}, - + # Connect between nodes {"from_node": 0, "from_socket": "Mesh", "to_node": 1, "to_socket": "Mesh"}, - + # Connect to output {"from_node": 1, "from_socket": "Points", "to_node": "output", "to_socket": "Geometry"} ] - + # Define input interfaces with default values input_sockets = [ {"name": "Geometry", "type": "NodeSocketGeometry"}, {"name": "Radius", "type": "NodeSocketFloat", "value": 2.0}, {"name": "Density", "type": "NodeSocketFloat", "value": 50.0} ] - + # Create geometry nodes result = complete_geometry_node( - object_name="SpherePoints", - nodes=nodes, + object_name="SpherePoints", + nodes=nodes, links=links, input_sockets=input_sockets, ) ``` - + returns: Detailed data containing operation status and created node network information """ try: blender = get_blender_connection() - + # Check if geometry nodes feature is enabled if not _geometry_nodes_enabled: return "Geometry nodes feature is disabled. Please enable this feature in the Blender MCP panel." - + result = blender.send_command("complete_geometry_node", { "object_name": object_name, "nodes": nodes, "links": links, "input_sockets": input_sockets, }) - + return json.dumps(result, indent=2) except Exception as e: logger.error(f"Error completing geometry node network: {str(e)}") return f"Error completing geometry node network: {str(e)}" + @mcp.tool() def get_geometry_nodes_status(ctx: Context) -> str: """ @@ -969,12 +1301,116 @@ def get_geometry_nodes_status(ctx: Context) -> str: result = blender.send_command("get_geometry_nodes_status") enabled = result.get("enabled", False) message = result.get("message", "") - + return message except Exception as e: logger.error(f"Error checking geometry nodes status: {str(e)}") return f"Error checking geometry nodes status: {str(e)}" + +@mcp.prompt() +def asset_creation_strategy() -> str: + """Defines the preferred strategy for creating assets in Blender""" + return """When creating 3D content in Blender, always start by checking if integrations are available: + + 0. Before anything, always check the scene from get_scene_info() + 1. First use the following tools to verify if the following integrations are enabled: + 1. PolyHaven + Use get_polyhaven_status() to verify its status + If PolyHaven is enabled: + - For objects/models: Use download_polyhaven_asset() with asset_type="models" + - For materials/textures: Use download_polyhaven_asset() with asset_type="textures" + - For environment lighting: Use download_polyhaven_asset() with asset_type="hdris" + 2. Sketchfab + Sketchfab is good at Realistic models, and has a wider variety of models than PolyHaven. + Use get_sketchfab_status() to verify its status + If Sketchfab is enabled: + - For objects/models: First search using search_sketchfab_models() with your query + - Then download specific models using download_sketchfab_model() with the UID + - Note that only downloadable models can be accessed, and API key must be properly configured + - Sketchfab has a wider variety of models than PolyHaven, especially for specific subjects + 3. Hyper3D(Rodin) + Hyper3D Rodin is good at generating 3D models for single item. + So don't try to: + 1. Generate the whole scene with one shot + 2. Generate ground using Hyper3D + 3. Generate parts of the items separately and put them together afterwards + + Use get_hyper3d_status() to verify its status + If Hyper3D is enabled: + - For objects/models, do the following steps: + 1. Create the model generation task + - Use generate_hyper3d_model_via_images() if image(s) is/are given + - Use generate_hyper3d_model_via_text() if generating 3D asset using text prompt + If key type is free_trial and insufficient balance error returned, tell the user that the free trial key can only generated limited models everyday, they can choose to: + - Wait for another day and try again + - Go to hyper3d.ai to find out how to get their own API key + - Go to fal.ai to get their own private API key + 2. Poll the status + - Use poll_rodin_job_status() to check if the generation task has completed or failed + 3. Import the asset + - Use import_generated_asset() to import the generated GLB model the asset + 4. After importing the asset, ALWAYS check the world_bounding_box of the imported mesh, and adjust the mesh's location and size + Adjust the imported mesh's location, scale, rotation, so that the mesh is on the right spot. + + You can reuse assets previous generated by running python code to duplicate the object, without creating another generation task. + 4. Hunyuan3D + Hunyuan3D is good at generating 3D models for single item. + So don't try to: + 1. Generate the whole scene with one shot + 2. Generate ground using Hunyuan3D + 3. Generate parts of the items separately and put them together afterwards + + Use get_hunyuan3d_status() to verify its status + If Hunyuan3D is enabled: + if Hunyuan3D mode is "OFFICIAL_API": + - For objects/models, do the following steps: + 1. Create the model generation task + - Use generate_hunyuan3d_model by providing either a **text description** OR an **image(local or urls) reference**. + - Go to cloud.tencent.com out how to get their own SecretId and SecretKey + 2. Poll the status + - Use poll_hunyuan_job_status() to check if the generation task has completed or failed + 3. Import the asset + - Use import_generated_asset_hunyuan() to import the generated OBJ model the asset + if Hunyuan3D mode is "LOCAL_API": + - For objects/models, do the following steps: + 1. Create the model generation task + - Use generate_hunyuan3d_model if image (local or urls) or text prompt is given and import the asset + + You can reuse assets previous generated by running python code to duplicate the object, without creating another generation task. + 5. Geometry Nodes + Use get_geometry_nodes_status() to verify the status of geometry nodes + If the geometry nodes feature is enabled, you must strictly follow these steps to create a node network: + 1. Query available nodes: Use get_node_info(output_format='text') to get an overview of all available nodes + 2. Get detailed information: Use get_node_info(node_type_name=['needed_nodes'], include_details=True, output_format='json') to get node details, including socket names and types + 3. Build network definition: + - Create nodes list: [{"type": "NodeType", "location": [x, y], "inputs": {}, "properties": {}, "label": "Label"}] + - Create links list: [{"from_node": source_node, "from_socket": "from_socket", "to_node": target_node, "to_socket": "to_socket"}] + - Use "input" and "output" as special identifiers to reference input/output nodes + 4. Direct creation: Use complete_geometry_node(object_name="name", nodes=nodes, links=links, input_sockets=input_sockets) + - No need to create objects in advance, the function will automatically create objects and modifiers + - The nodes list doesn't need to manually specify GroupNodeInput/Output as default input/output nodes will be created automatically + - You can specify input_sockets with default values that will be set automatically + + 3. Always check the world_bounding_box for each item so that: + - Ensure that all objects that should not be clipping are not clipping. + - Items have right spatial relationship. + + 4. Recommended asset source priority: + - For specific existing objects: First try Sketchfab, then PolyHaven + - For generic objects/furniture: First try PolyHaven, then Sketchfab + - For custom or unique items not available in libraries: Use Hyper3D Rodin or Hunyuan3D + - For environment lighting: Use PolyHaven HDRIs + - For materials/textures: Use PolyHaven textures + + Only fall back to scripting when: + - PolyHaven, Sketchfab, Hyper3D, and Hunyuan3D are all disabled + - A simple primitive is explicitly requested + - No suitable asset exists in any of the libraries + - Hyper3D Rodin or Hunyuan3D failed to generate the desired asset + - The task specifically requires a basic material/color + """ + # Main execution def main(): @@ -982,4 +1418,4 @@ def main(): mcp.run() if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/src/blender_mcp/telemetry.py b/src/blender_mcp/telemetry.py new file mode 100644 index 000000000..0f7c6ad60 --- /dev/null +++ b/src/blender_mcp/telemetry.py @@ -0,0 +1,315 @@ +""" +Privacy-focused, anonymous telemetry for Blender MCP +Tracks tool usage, DAU/MAU, and performance metrics +""" + +import contextlib +import json +import logging +import os +import platform +import queue +import sys +import threading +import time +import uuid +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Any + +try: + from supabase import create_client, Client + HAS_SUPABASE = True +except ImportError: + HAS_SUPABASE = False + +try: + import tomli +except ImportError: + try: + import tomllib as tomli + except ImportError: + tomli = None + +logger = logging.getLogger("blender-mcp-telemetry") + + +def get_package_version() -> str: + """Get version from pyproject.toml""" + try: + pyproject_path = Path(__file__).parent.parent.parent.parent / "pyproject.toml" + if pyproject_path.exists(): + if tomli: + with open(pyproject_path, "rb") as f: + data = tomli.load(f) + return data["project"]["version"] + except Exception: + pass + return "unknown" + + +MCP_VERSION = get_package_version() + + +class EventType(str, Enum): + """Types of telemetry events""" + STARTUP = "startup" + TOOL_EXECUTION = "tool_execution" + PROMPT_SENT = "prompt_sent" + CONNECTION = "connection" + ERROR = "error" + + +@dataclass +class TelemetryEvent: + """Structure for telemetry events""" + event_type: EventType + customer_uuid: str + session_id: str + timestamp: float + version: str + platform: str + + # Optional fields + tool_name: str | None = None + prompt_text: str | None = None + success: bool = True + duration_ms: float | None = None + error_message: str | None = None + blender_version: str | None = None + metadata: dict[str, Any] | None = None + + +class TelemetryCollector: + """Main telemetry collection class""" + + def __init__(self): + """Initialize telemetry collector""" + # Import config here to avoid circular imports + from .config import telemetry_config + self.config = telemetry_config + + # Check if disabled via environment variables + if self._is_disabled(): + self.config.enabled = False + logger.warning("Telemetry disabled via environment variable") + + # Generate or load customer UUID + self._customer_uuid: str = self._get_or_create_uuid() + self._session_id: str = str(uuid.uuid4()) + + # Rate limiting tracking + self._event_timestamps: list[float] = [] + self._rate_limit_lock = threading.Lock() + + # Background queue and worker + self._queue: "queue.Queue[TelemetryEvent]" = queue.Queue(maxsize=1000) + self._worker: threading.Thread = threading.Thread( + target=self._worker_loop, daemon=True + ) + self._worker.start() + + logger.warning(f"Telemetry initialized (enabled={self.config.enabled}, has_supabase={HAS_SUPABASE}, customer_uuid={self._customer_uuid})") + + def _is_disabled(self) -> bool: + """Check if telemetry is disabled via environment variables""" + disable_vars = [ + "DISABLE_TELEMETRY", + "BLENDER_MCP_DISABLE_TELEMETRY", + "MCP_DISABLE_TELEMETRY" + ] + + for var in disable_vars: + if os.environ.get(var, "").lower() in ("true", "1", "yes", "on"): + return True + return False + + def _get_data_directory(self) -> Path: + """Get directory for storing telemetry data""" + if sys.platform == "win32": + base_dir = Path(os.environ.get('APPDATA', Path.home() / 'AppData' / 'Roaming')) + elif sys.platform == "darwin": + base_dir = Path.home() / 'Library' / 'Application Support' + else: # Linux + base_dir = Path(os.environ.get('XDG_DATA_HOME', Path.home() / '.local' / 'share')) + + data_dir = base_dir / 'BlenderMCP' + data_dir.mkdir(parents=True, exist_ok=True) + return data_dir + + def _get_or_create_uuid(self) -> str: + """Get or create anonymous customer UUID""" + try: + data_dir = self._get_data_directory() + uuid_file = data_dir / "customer_uuid.txt" + + if uuid_file.exists(): + customer_uuid = uuid_file.read_text(encoding="utf-8").strip() + if customer_uuid: + return customer_uuid + + # Create new UUID + customer_uuid = str(uuid.uuid4()) + uuid_file.write_text(customer_uuid, encoding="utf-8") + + # Set restrictive permissions on Unix + if sys.platform != "win32": + os.chmod(uuid_file, 0o600) + + return customer_uuid + except Exception as e: + logger.debug(f"Failed to persist UUID: {e}") + return str(uuid.uuid4()) + + def record_event( + self, + event_type: EventType, + tool_name: str | None = None, + prompt_text: str | None = None, + success: bool = True, + duration_ms: float | None = None, + error_message: str | None = None, + blender_version: str | None = None, + metadata: dict[str, Any] | None = None + ): + """Record a telemetry event (non-blocking)""" + if not self.config.enabled: + logger.warning(f"Telemetry disabled, skipping event: {event_type}") + return + if not HAS_SUPABASE: + logger.warning(f"Supabase not available, skipping event: {event_type}") + return + + logger.warning(f"Recording telemetry event: {event_type}, tool={tool_name}") + + # Truncate prompt if needed + if prompt_text and not self.config.collect_prompts: + prompt_text = None # Don't collect prompts unless explicitly enabled + elif prompt_text and len(prompt_text) > self.config.max_prompt_length: + prompt_text = prompt_text[:self.config.max_prompt_length] + "..." + + # Truncate error messages + if error_message and len(error_message) > 200: + error_message = error_message[:200] + "..." + + event = TelemetryEvent( + event_type=event_type, + customer_uuid=self._customer_uuid, + session_id=self._session_id, + timestamp=time.time(), + version=MCP_VERSION, + platform=platform.system().lower(), + tool_name=tool_name, + prompt_text=prompt_text, + success=success, + duration_ms=duration_ms, + error_message=error_message, + blender_version=blender_version, + metadata=metadata + ) + + # Enqueue for background worker + try: + self._queue.put_nowait(event) + except queue.Full: + logger.debug("Telemetry queue full, dropping event") + + def _worker_loop(self): + """Background worker that sends telemetry""" + while True: + event = self._queue.get() + try: + self._send_event(event) + except Exception as e: + logger.debug(f"Telemetry send failed: {e}") + finally: + with contextlib.suppress(Exception): + self._queue.task_done() + + def _send_event(self, event: TelemetryEvent): + """Send event to Supabase""" + if not HAS_SUPABASE: + return + + try: + # Create Supabase client with explicit options + from supabase import ClientOptions + + options = ClientOptions( + auto_refresh_token=False, + persist_session=False + ) + + supabase: Client = create_client( + self.config.supabase_url, + self.config.supabase_anon_key, + options=options + ) + + # Prepare data for insertion + data = { + "customer_uuid": event.customer_uuid, + "session_id": event.session_id, + "event_type": event.event_type.value, + "tool_name": event.tool_name, + "prompt_text": event.prompt_text, + "success": event.success, + "duration_ms": event.duration_ms, + "error_message": event.error_message, + "version": event.version, + "platform": event.platform, + "blender_version": event.blender_version, + "metadata": event.metadata or {}, + "event_timestamp": int(event.timestamp), + } + + response = supabase.table("telemetry_events").insert(data, returning="minimal").execute() + logger.debug(f"Telemetry sent: {event.event_type}") + + except Exception as e: + logger.debug(f"Failed to send telemetry: {e}") + + +# Global telemetry instance +_telemetry_collector: TelemetryCollector | None = None + + +def get_telemetry() -> TelemetryCollector: + """Get the global telemetry collector instance""" + global _telemetry_collector + if _telemetry_collector is None: + _telemetry_collector = TelemetryCollector() + return _telemetry_collector + + +def record_tool_usage( + tool_name: str, + success: bool, + duration_ms: float, + error: str | None = None +): + """Convenience function to record tool usage""" + get_telemetry().record_event( + event_type=EventType.TOOL_EXECUTION, + tool_name=tool_name, + success=success, + duration_ms=duration_ms, + error_message=error + ) + + +def record_startup(blender_version: str | None = None): + """Record server startup event""" + get_telemetry().record_event( + event_type=EventType.STARTUP, + blender_version=blender_version + ) + + +def is_telemetry_enabled() -> bool: + """Check if telemetry is enabled""" + try: + return get_telemetry().config.enabled + except Exception: + return False diff --git a/src/blender_mcp/telemetry_decorator.py b/src/blender_mcp/telemetry_decorator.py new file mode 100644 index 000000000..578fd0f12 --- /dev/null +++ b/src/blender_mcp/telemetry_decorator.py @@ -0,0 +1,65 @@ +""" +Telemetry decorator for Blender MCP tools +""" + +import functools +import inspect +import logging +import time +from typing import Callable, Any + +from .telemetry import record_tool_usage + +logger = logging.getLogger("blender-mcp-telemetry") + + +def telemetry_tool(tool_name: str): + """Decorator to add telemetry tracking to MCP tools""" + def decorator(func: Callable) -> Callable: + @functools.wraps(func) + def sync_wrapper(*args, **kwargs) -> Any: + start_time = time.time() + success = False + error = None + + try: + result = func(*args, **kwargs) + success = True + return result + except Exception as e: + error = str(e) + raise + finally: + duration_ms = (time.time() - start_time) * 1000 + try: + record_tool_usage(tool_name, success, duration_ms, error) + except Exception as log_error: + logger.debug(f"Failed to record telemetry: {log_error}") + + @functools.wraps(func) + async def async_wrapper(*args, **kwargs) -> Any: + start_time = time.time() + success = False + error = None + + try: + result = await func(*args, **kwargs) + success = True + return result + except Exception as e: + error = str(e) + raise + finally: + duration_ms = (time.time() - start_time) * 1000 + try: + record_tool_usage(tool_name, success, duration_ms, error) + except Exception as log_error: + logger.debug(f"Failed to record telemetry: {log_error}") + + # Return appropriate wrapper based on function type + if inspect.iscoroutinefunction(func): + return async_wrapper + else: + return sync_wrapper + + return decorator diff --git a/uv.lock b/uv.lock index 7f7e6bc71..070ad17aa 100644 --- a/uv.lock +++ b/uv.lock @@ -28,14 +28,20 @@ wheels = [ [[package]] name = "blender-mcp" -version = "1.1.2" +version = "1.3" source = { editable = "." } dependencies = [ { name = "mcp", extra = ["cli"] }, + { name = "supabase" }, + { name = "tomli" }, ] [package.metadata] -requires-dist = [{ name = "mcp", extras = ["cli"], specifier = ">=1.3.0" }] +requires-dist = [ + { name = "mcp", extras = ["cli"], specifier = ">=1.3.0" }, + { name = "supabase", specifier = ">=2.0.0" }, + { name = "tomli", specifier = ">=2.0.0" }, +] [[package]] name = "certifi" @@ -46,6 +52,88 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283 }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504 }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811 }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402 }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217 }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079 }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475 }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829 }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211 }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036 }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184 }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344 }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560 }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613 }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374 }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597 }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574 }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971 }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972 }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078 }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076 }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820 }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635 }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +] + [[package]] name = "click" version = "8.1.8" @@ -67,6 +155,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004 }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667 }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807 }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615 }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800 }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707 }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541 }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464 }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838 }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596 }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782 }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381 }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988 }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451 }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007 }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012 }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728 }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078 }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460 }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237 }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344 }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564 }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415 }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457 }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074 }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569 }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941 }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339 }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315 }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331 }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248 }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089 }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029 }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222 }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280 }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958 }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714 }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970 }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236 }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642 }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126 }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573 }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695 }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720 }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740 }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163 }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474 }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132 }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992 }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944 }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957 }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447 }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528 }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, +] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -85,6 +250,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779 }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + [[package]] name = "httpcore" version = "1.0.7" @@ -113,6 +300,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, ] +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + [[package]] name = "httpx-sse" version = "0.4.0" @@ -122,6 +314,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + [[package]] name = "idna" version = "3.10" @@ -177,93 +378,411 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153 }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993 }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607 }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847 }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616 }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333 }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239 }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618 }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655 }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245 }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523 }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129 }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999 }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711 }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504 }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422 }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050 }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153 }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604 }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715 }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332 }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212 }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671 }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491 }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322 }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694 }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715 }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189 }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845 }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374 }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345 }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940 }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229 }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308 }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037 }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023 }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877 }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467 }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834 }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545 }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305 }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363 }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375 }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346 }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107 }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592 }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024 }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484 }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579 }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654 }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511 }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895 }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073 }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226 }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135 }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117 }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472 }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342 }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082 }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704 }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355 }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259 }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903 }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365 }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062 }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683 }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254 }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967 }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085 }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713 }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915 }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077 }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114 }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442 }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885 }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588 }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966 }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618 }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539 }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345 }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934 }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243 }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878 }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452 }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312 }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935 }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385 }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777 }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104 }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503 }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128 }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410 }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205 }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084 }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667 }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590 }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112 }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194 }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510 }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395 }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520 }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479 }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903 }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333 }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411 }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940 }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087 }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368 }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326 }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065 }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475 }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324 }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877 }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824 }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558 }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339 }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895 }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862 }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376 }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272 }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774 }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731 }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193 }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023 }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507 }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804 }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "postgrest" +version = "2.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, + { name = "strenum", marker = "python_full_version < '3.11'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/c2/9587c88b544dfadd8a09da8d7adf350fafc67dcaa8ba1cd653e0065113d0/postgrest-2.22.0.tar.gz", hash = "sha256:e9d77ed6c82918f8b67d48c963f52de06c71cd1351b64881f239c54a19f6420a", size = 13640 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/fc/508536c4b9e63ef794aec29e9d1f6b17081bb942f72e71f756b130cf32c5/postgrest-2.22.0-py3-none-any.whl", hash = "sha256:cbb5ded1df3806593d95c430109fe292201c23f627629af01a9f67381d65b370", size = 21394 }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534 }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526 }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263 }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012 }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491 }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319 }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856 }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241 }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552 }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778 }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047 }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093 }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638 }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229 }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208 }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777 }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647 }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929 }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778 }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144 }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030 }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252 }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064 }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429 }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727 }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097 }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084 }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637 }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064 }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061 }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037 }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324 }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505 }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242 }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474 }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575 }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736 }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019 }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376 }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988 }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615 }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066 }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655 }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789 }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, +] + [[package]] name = "pydantic" -version = "2.10.6" +version = "2.12.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, + { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431 }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.41.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197 }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909 }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905 }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938 }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710 }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445 }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875 }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329 }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658 }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777 }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705 }, + { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464 }, + { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497 }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062 }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301 }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728 }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238 }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424 }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047 }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163 }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585 }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109 }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078 }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737 }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160 }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883 }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026 }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043 }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699 }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121 }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590 }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869 }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169 }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165 }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067 }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997 }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187 }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204 }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536 }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132 }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483 }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688 }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807 }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669 }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629 }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049 }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409 }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635 }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284 }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566 }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809 }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119 }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398 }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735 }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209 }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324 }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515 }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819 }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866 }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034 }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022 }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495 }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131 }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236 }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573 }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467 }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754 }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754 }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115 }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400 }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070 }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277 }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608 }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614 }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904 }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538 }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183 }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542 }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897 }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739 }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549 }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093 }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971 }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939 }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400 }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840 }, + { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135 }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721 }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608 }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986 }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516 }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146 }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296 }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386 }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775 }, ] [[package]] @@ -288,6 +807,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -297,6 +830,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, ] +[[package]] +name = "realtime" +version = "2.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/60/af9bcff4bd3a00c25603ebba9bb2a06a515e01fb966e81e738284c1c9da1/realtime-2.22.0.tar.gz", hash = "sha256:116b331bcf34bf604e38edf6ac85d89cb7f98a9e973fd0c54a6baaeaee7404f7", size = 18527 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/85/bdbb72a1f16e5d333bb250f4eed1edcc616f50ef8dec56ef324974a790cc/realtime-2.22.0-py3-none-any.whl", hash = "sha256:a599b7450f876f4ebe95aa1ccb3f3128ac8bea7e468950dc947708e2e3779015", size = 22130 }, +] + [[package]] name = "rich" version = "13.9.4" @@ -354,6 +901,124 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/94/8af675a62e3c91c2dee47cf92e602cfac86e8767b1a1ac3caf1b327c2ab0/starlette-0.46.0-py3-none-any.whl", hash = "sha256:913f0798bd90ba90a9156383bcf1350a17d6259451d0d8ee27fc0cf2db609038", size = 71991 }, ] +[[package]] +name = "storage3" +version = "2.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/a7/d9ad918c4c4d05111be2fabcd361bc73000fbb832ce9556c7be9637f6518/storage3-2.22.0.tar.gz", hash = "sha256:199a67c2b1ac595191617ff2d7bcd0ef6440126e4335a6aaaf9df7881ccd3842", size = 9812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/03/61fbad9f8091e05af045125cfa3f5db377ffc51634ba6fbd889ed1d70f80/storage3-2.22.0-py3-none-any.whl", hash = "sha256:fd31d884bfd590866e2b1897f3b2f26f737a836155a13c9a642dbc5c353e49df", size = 18882 }, +] + +[[package]] +name = "strenum" +version = "0.4.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ad/430fb60d90e1d112a62ff57bdd1f286ec73a2a0331272febfddd21f330e1/StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff", size = 23384 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659", size = 8851 }, +] + +[[package]] +name = "supabase" +version = "2.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "postgrest" }, + { name = "realtime" }, + { name = "storage3" }, + { name = "supabase-auth" }, + { name = "supabase-functions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/c6/b71bd14057e7d406a688aec7aee2e151f72344a086c0f4eee60f8bac06b9/supabase-2.22.0.tar.gz", hash = "sha256:c2dda9cc712db69ab4f690092581cff0b8978c268f782a3c16663c9ff0094473", size = 9323 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/a9/e5c30975edb0cd114f7463c76f0815931d255ee128ab4ea6e1650d8e23c2/supabase-2.22.0-py3-none-any.whl", hash = "sha256:4d50b32b07b07439f69db75c1c1b013446167fd2aa747fbbb3b094084b5a52f4", size = 16357 }, +] + +[[package]] +name = "supabase-auth" +version = "2.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, + { name = "pyjwt", extra = ["crypto"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/56/5f2d2a6da345d4da4f6e8d74285c670b8499de7634eb88f65dc403b5f13e/supabase_auth-2.22.0.tar.gz", hash = "sha256:6a3db812102e0ab1a399996d492f05f68e07ca6510291758678be7b8c5345ce0", size = 35491 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/0a/2df24a68db3cdaebfd02d879152a0ae7a7b6b38711610cbaa25e6875e8a4/supabase_auth-2.22.0-py3-none-any.whl", hash = "sha256:bea3808b9a17b883f6aa56abd55ba1b00c311e6f4a6e42ed9e03321caa9fe71f", size = 43940 }, +] + +[[package]] +name = "supabase-functions" +version = "2.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "strenum" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/6f/d684f70d2bbbd181f5a9a7b60a9b4d0045187d0f8e9715e1d07d3da75220/supabase_functions-2.22.0.tar.gz", hash = "sha256:dfc04c0c751629813e75153124b7f1bc81ff0128d058f6deaed2f124782416fa", size = 4649 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9b/091d894b282b49bcd49c735bb259631431f40b8448ac3c3e7a261ac05f74/supabase_functions-2.22.0-py3-none-any.whl", hash = "sha256:c92d1b832499abfa8bde248d674aeb5f03b511d29959bf3d16afcdab824b290c", size = 8660 }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 }, +] + [[package]] name = "typer" version = "0.15.2" @@ -371,11 +1036,23 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, ] [[package]] @@ -391,3 +1068,188 @@ sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec wheels = [ { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, ] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080 }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312 }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319 }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631 }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016 }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426 }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360 }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830 }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423 }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082 }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330 }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878 }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883 }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252 }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521 }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958 }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918 }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388 }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828 }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109 }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343 }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599 }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207 }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517 }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495 }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400 }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545 }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598 }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893 }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240 }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965 }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026 }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637 }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082 }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811 }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223 }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118 }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852 }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012 }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607 }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027 }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963 }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406 }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581 }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924 }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890 }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819 }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601 }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072 }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311 }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094 }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944 }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804 }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858 }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637 }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000 }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338 }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909 }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940 }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825 }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705 }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518 }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267 }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797 }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535 }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324 }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803 }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220 }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589 }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213 }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330 }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980 }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424 }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821 }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243 }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361 }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036 }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671 }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059 }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356 }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331 }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590 }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316 }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431 }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555 }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965 }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205 }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209 }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966 }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312 }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967 }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949 }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818 }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626 }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129 }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776 }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879 }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996 }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047 }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947 }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943 }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715 }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857 }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520 }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504 }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282 }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080 }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696 }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121 }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080 }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661 }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645 }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361 }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451 }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814 }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799 }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990 }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292 }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888 }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223 }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981 }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303 }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820 }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203 }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173 }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562 }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828 }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551 }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512 }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400 }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140 }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473 }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056 }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292 }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171 }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814 }, +] From 802fb12365caf65151f57a8fd37f53ebce0995d2 Mon Sep 17 00:00:00 2001 From: Duke Date: Sun, 11 Jan 2026 12:54:11 +0800 Subject: [PATCH 3/4] fix: add geo-feat unregister --- addon.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addon.py b/addon.py index 444fdcd32..cb4e34a22 100644 --- a/addon.py +++ b/addon.py @@ -3638,6 +3638,7 @@ def unregister(): del bpy.types.Scene.blendermcp_hunyuan3d_num_inference_steps del bpy.types.Scene.blendermcp_hunyuan3d_guidance_scale del bpy.types.Scene.blendermcp_hunyuan3d_texture + del bpy.types.Scene.blendermcp_use_geometry_nodes print("BlenderMCP addon unregistered") From e0d4c9ba07aeecdfea94c4feb0774060cb16a43a Mon Sep 17 00:00:00 2001 From: Duke Date: Sun, 11 Jan 2026 13:18:15 +0800 Subject: [PATCH 4/4] perf: simplify geo-node feature code --- addon.py | 845 ++++++++++++++++++++++--------------------------------- 1 file changed, 331 insertions(+), 514 deletions(-) diff --git a/addon.py b/addon.py index cb4e34a22..b1b95cc43 100644 --- a/addon.py +++ b/addon.py @@ -44,58 +44,58 @@ @dataclass class NodeDefinition: """Node definition data class""" - type: str # Node type name - location: List[float] = field(default_factory=lambda: [0.0, 0.0]) # Node position [x, y] - label: str = "" # Node label - inputs: Dict[str, Any] = field(default_factory=dict) # Input values dictionary - properties: Dict[str, Any] = field(default_factory=dict) # Node properties parameter dictionary + type: str + location: List[float] = field(default_factory=lambda: [0.0, 0.0]) + label: str = "" + inputs: Dict[str, Any] = field(default_factory=dict) + properties: Dict[str, Any] = field(default_factory=dict) @dataclass class NodeLink: """Node connection data class""" - from_node: Union[str, int] # Source node name or index - from_socket: Union[str, int] # Source socket name or index - to_node: Union[str, int] # Target node name or index - to_socket: Union[str, int] # Target socket name or index + from_node: Union[str, int] + from_socket: Union[str, int] + to_node: Union[str, int] + to_socket: Union[str, int] @dataclass class GeometryNodeNetwork: """Geometry node network data class""" - object_name: str # Object name - nodes: List[NodeDefinition] = field(default_factory=list) # Node list - links: List[NodeLink] = field(default_factory=list) # Connection list - input_sockets: List[Dict[str, str]] = field(default_factory=list) # Input interface definition - output_sockets: List[Dict[str, str]] = field(default_factory=list) # Output interface definition + object_name: str + nodes: List[NodeDefinition] = field(default_factory=list) + links: List[NodeLink] = field(default_factory=list) + input_sockets: List[Dict[str, str]] = field(default_factory=list) + output_sockets: List[Dict[str, str]] = field(default_factory=list) @dataclass class SocketInfo: """Socket information data class""" - name: str # Socket name - type: str # Socket type - description: str # Socket description - identifier: str # Socket identifier - enabled: bool # Whether enabled - hide: bool # Whether hidden - default_value: Any = None # Default value (if any) + name: str + type: str + description: str = "" + identifier: str = "" + enabled: bool = True + hide: bool = False + default_value: Any = None @dataclass class PropertyInfo: """Node property information data class""" - identifier: str # Property identifier - name: str # Property name - description: str # Property description - type: str # Property type - default_value: Any = None # Default value (if any) - enum_items: List[Dict[str, str]] = field(default_factory=list) # Enum options (if any) + identifier: str + name: str + description: str = "" + type: str = "" + default_value: Any = None + enum_items: List[Dict[str, str]] = field(default_factory=list) @dataclass class NodeInfo: """Node information data class""" - name: str # Node type name (identifier used to create the node) - description: str # Node description - inputs: List[SocketInfo] = field(default_factory=list) # Input socket information - outputs: List[SocketInfo] = field(default_factory=list) # Output socket information - properties: List[PropertyInfo] = field(default_factory=list) # Node property information + name: str + description: str = "" + inputs: List[SocketInfo] = field(default_factory=list) + outputs: List[SocketInfo] = field(default_factory=list) + properties: List[PropertyInfo] = field(default_factory=list) # endregion @@ -2431,293 +2431,208 @@ def import_generated_asset_hunyuan_ai(self, name: str , zip_file_url: str): # region GeometryNodeCreator def complete_geometry_node(self, object_name, nodes, links, input_sockets=None): - """Complete geometry node network creation - - Args: - object_name: Object name - nodes: List of node definitions - links: List of node connections - input_sockets: Node group input interface definitions [{"name": "Name", "type": "Type", "value": "DefaultValue"}] - - Returns: - dict: Dictionary containing operation status and related information - """ + """Complete geometry node network creation""" try: - obj = bpy.data.objects.get(object_name) - if not obj: - result = self._create_geometry_nodes_object(object_name) - if "error" in result: - return result - obj = bpy.data.objects.get(object_name) - - # Find geometry nodes modifier - geometry_modifier = None - for modifier in obj.modifiers: - if modifier.type == 'NODES': - geometry_modifier = modifier - break - - if geometry_modifier and geometry_modifier.node_group: - old_node_group_name = geometry_modifier.node_group.name - geometry_modifier.node_group = None - - # Try to delete the old node group - old_node_group = bpy.data.node_groups.get(old_node_group_name) - if old_node_group: - bpy.data.node_groups.remove(old_node_group) - - # If there's no geometry nodes modifier, create one - if not geometry_modifier: - geometry_modifier = obj.modifiers.new(name="GeometryNodes", type='NODES') - - # Create a new node group - node_group = bpy.data.node_groups.new(name=f"{object_name}_geometry", type='GeometryNodeTree') - if IS_BLENDER_4: - node_group.is_modifier = True - - # Set the node group for the modifier - geometry_modifier.node_group = node_group - - has_input_node = False - has_output_node = False - for node_data in nodes: - node_type = node_data.get("type", "") - if node_type in ['NodeGroupInput', 'GroupInput']: - has_input_node = True - elif node_type in ['NodeGroupOutput', 'GroupOutput']: - has_output_node = True - - self._setup_node_group_interface(node_group, input_sockets) - - # First create all nodes - created_nodes = [] - - input_node = None - output_node = None - - if not has_input_node: - input_node = node_group.nodes.new('NodeGroupInput') - input_node.location = (-300, 0) - - if not has_output_node: - output_node = node_group.nodes.new('NodeGroupOutput') - output_node.location = (500, 0) - - for node_data in nodes: - try: - node_type = node_data["type"] - - node = node_group.nodes.new(node_type) - - if "location" in node_data: - node.location = node_data["location"] - - if "label" in node_data: - node.label = node_data["label"] - - if "properties" in node_data: - for prop_name, prop_value in node_data["properties"].items(): - if hasattr(node, prop_name): - try: - setattr(node, prop_name, prop_value) - except Exception as prop_error: - print(f"Error setting property {prop_name}: {prop_error}") - - if "inputs" in node_data: - for input_name, input_value in node_data["inputs"].items(): - for input in node.inputs: - if input.name == input_name and hasattr(input, 'default_value'): - try: - if hasattr(input.default_value, '__len__'): - for i, val in enumerate(input_value): - if i < len(input.default_value): - input.default_value[i] = val - else: - input.default_value = input_value - except Exception as input_error: - print(f"Error setting input value: {input_error}") - - node_info = { - "name": node.name, - "type": node.type, - "location": [node.location.x, node.location.y] - } - - created_nodes.append(node_info) - except Exception as node_error: - return {"error": f"Error creating node {node_data['type']}: {str(node_error)}"} - - # Create links - created_links = [] - - for link_data in links: - try: - # Find nodes by index or name - from_node_id = link_data["from_node"] - to_node_id = link_data["to_node"] - - from_node = None - to_node = None - - # Special handling for "input" and "output" identifiers - if from_node_id == "input": - from_node = input_node - elif from_node_id == "output": - from_node = output_node - elif isinstance(from_node_id, int): - # Assume integer index points to created node - if 0 <= from_node_id < len(created_nodes): - from_node = node_group.nodes.get(created_nodes[from_node_id]["name"]) - else: - # Find by name - from_node = node_group.nodes.get(from_node_id) - - if to_node_id == "input": - to_node = input_node - elif to_node_id == "output": - to_node = output_node - elif isinstance(to_node_id, int): - # Assume integer index points to created node - if 0 <= to_node_id < len(created_nodes): - to_node = node_group.nodes.get(created_nodes[to_node_id]["name"]) - else: - # Find by name - to_node = node_group.nodes.get(to_node_id) + obj = self._get_or_create_object(object_name) + if isinstance(obj, dict) and "error" in obj: + return obj + + geometry_modifier = self._setup_geometry_modifier(obj, object_name) + node_group = geometry_modifier.node_group + + input_node, output_node = self._create_default_nodes(node_group, nodes) + created_nodes = self._create_nodes(node_group, nodes) + created_links = self._create_links(node_group, links, created_nodes, input_node, output_node) + + if input_sockets: + self._set_modifier_parameters(geometry_modifier, node_group, input_sockets) - if not from_node: - return {"error": f"Could not find source node: {from_node_id}"} + return { + "status": "success", + "object_name": object_name, + "modifier_name": geometry_modifier.name, + "node_group_name": node_group.name, + "nodes": created_nodes, + "links": created_links + } + except Exception as e: + return {"error": f"Error completing geometry node network: {str(e)}"} - if not to_node: - return {"error": f"Could not find target node: {to_node_id}"} + def _get_or_create_object(self, object_name): + """Get existing object or create new one""" + obj = bpy.data.objects.get(object_name) + if not obj: + result = self._create_geometry_nodes_object(object_name) + if "error" in result: + return result + obj = bpy.data.objects.get(object_name) + return obj - # Find socket - from_socket_id = link_data["from_socket"] - to_socket_id = link_data["to_socket"] + def _setup_geometry_modifier(self, obj, object_name): + """Setup geometry nodes modifier""" + geometry_modifier = next((mod for mod in obj.modifiers if mod.type == 'NODES'), None) + + if geometry_modifier and geometry_modifier.node_group: + old_node_group = geometry_modifier.node_group + geometry_modifier.node_group = None + if old_node_group: + bpy.data.node_groups.remove(old_node_group) - from_socket = None - to_socket = None + if not geometry_modifier: + geometry_modifier = obj.modifiers.new(name="GeometryNodes", type='NODES') - # Find socket by index or name - if isinstance(from_socket_id, int): - if 0 <= from_socket_id < len(from_node.outputs): - from_socket = from_node.outputs[from_socket_id] - else: - # Search by name - for socket in from_node.outputs: - if socket.name == from_socket_id: - from_socket = socket - break - - if isinstance(to_socket_id, int): - if 0 <= to_socket_id < len(to_node.inputs): - to_socket = to_node.inputs[to_socket_id] - else: - # Search by name - for socket in to_node.inputs: - if socket.name == to_socket_id: - to_socket = socket - break - - if not from_socket: - return {"error": f"Could not find source socket: {from_socket_id}"} - - if not to_socket: - return {"error": f"Could not find target socket: {to_socket_id}"} - - # Create connection - link = node_group.links.new(from_socket, to_socket) - - # Record created link - link_info = { - "from_node": from_node.name, - "from_socket": from_socket.name, - "to_node": to_node.name, - "to_socket": to_socket.name - } + node_group = bpy.data.node_groups.new(name=f"{object_name}_geometry", type='GeometryNodeTree') + if IS_BLENDER_4: + node_group.is_modifier = True + + geometry_modifier.node_group = node_group + self._setup_node_group_interface(node_group, None) + return geometry_modifier + + def _create_default_nodes(self, node_group, nodes): + """Create default input/output nodes if not present""" + has_input = any(node_data.get("type") in ['NodeGroupInput', 'GroupInput'] for node_data in nodes) + has_output = any(node_data.get("type") in ['NodeGroupOutput', 'GroupOutput'] for node_data in nodes) + + input_node = None + output_node = None + + if not has_input: + input_node = node_group.nodes.new('NodeGroupInput') + input_node.location = (-300, 0) + + if not has_output: + output_node = node_group.nodes.new('NodeGroupOutput') + output_node.location = (500, 0) + + return input_node, output_node - created_links.append(link_info) - except Exception as link_error: - return {"error": f"Error creating link: {str(link_error)}"} + def _create_nodes(self, node_group, nodes): + """Create nodes from definitions""" + created_nodes = [] + + for node_data in nodes: + try: + node = node_group.nodes.new(node_data["type"]) + + if "location" in node_data: + node.location = node_data["location"] + if "label" in node_data: + node.label = node_data["label"] + + self._set_node_properties(node, node_data.get("properties", {})) + self._set_node_inputs(node, node_data.get("inputs", {})) + + created_nodes.append({ + "name": node.name, + "type": node.type, + "location": [node.location.x, node.location.y] + }) + except Exception as e: + raise Exception(f"Error creating node {node_data['type']}: {str(e)}") + + return created_nodes - # If input sockets are provided and contain values, set modifier parameters - if input_sockets: + def _set_node_properties(self, node, properties): + """Set node properties""" + for prop_name, prop_value in properties.items(): + if hasattr(node, prop_name): try: - # Find the NodeGroupInput node - group_input_node = None - for node in node_group.nodes: - if node.type == 'GROUP_INPUT': - group_input_node = node - break - - # Create one if not found - if not group_input_node: - group_input_node = node_group.nodes.new('NodeGroupInput') - - # Get socket information directly from the NodeGroupInput node - socket_dict = {} - for i, output in enumerate(group_input_node.outputs): - socket_dict[output.name] = i + setattr(node, prop_name, prop_value) + except Exception as e: + print(f"Error setting property {prop_name}: {e}") - # Output debug information - print(f"Sockets from NodeGroupInput: {socket_dict}") + def _set_node_inputs(self, node, inputs): + """Set node input values""" + for input_name, input_value in inputs.items(): + for input_socket in node.inputs: + if input_socket.name == input_name and hasattr(input_socket, 'default_value'): + try: + if hasattr(input_socket.default_value, '__len__'): + for i, val in enumerate(input_value): + if i < len(input_socket.default_value): + input_socket.default_value[i] = val + else: + input_socket.default_value = input_value + except Exception as e: + print(f"Error setting input value: {e}") + + def _find_node(self, node_group, node_id, created_nodes, input_node, output_node): + """Find node by ID (name, index, or special identifier)""" + if node_id == "input": + return input_node + elif node_id == "output": + return output_node + elif isinstance(node_id, int) and 0 <= node_id < len(created_nodes): + return node_group.nodes.get(created_nodes[node_id]["name"]) + else: + return node_group.nodes.get(node_id) - # Set modifier parameter values - for socket in input_sockets: - if "value" in socket and socket.get("type") != "NodeSocketGeometry": - socket_name = socket.get("name") + def _find_socket(self, node, socket_id, is_output=True): + """Find socket by ID (name or index)""" + sockets = node.outputs if is_output else node.inputs + + if isinstance(socket_id, int) and 0 <= socket_id < len(sockets): + return sockets[socket_id] + else: + return next((s for s in sockets if s.name == socket_id), None) - if socket_name in socket_dict: - socket_index = socket_dict[socket_name] - socket_key = f"Socket_{socket_index}" + def _create_links(self, node_group, links, created_nodes, input_node, output_node): + """Create links between nodes""" + created_links = [] + + for link_data in links: + try: + from_node = self._find_node(node_group, link_data["from_node"], created_nodes, input_node, output_node) + to_node = self._find_node(node_group, link_data["to_node"], created_nodes, input_node, output_node) + + if not from_node: + raise Exception(f"Could not find source node: {link_data['from_node']}") + if not to_node: + raise Exception(f"Could not find target node: {link_data['to_node']}") + + from_socket = self._find_socket(from_node, link_data["from_socket"], True) + to_socket = self._find_socket(to_node, link_data["to_socket"], False) + + if not from_socket: + raise Exception(f"Could not find source socket: {link_data['from_socket']}") + if not to_socket: + raise Exception(f"Could not find target socket: {link_data['to_socket']}") + + node_group.links.new(from_socket, to_socket) + + created_links.append({ + "from_node": from_node.name, + "from_socket": from_socket.name, + "to_node": to_node.name, + "to_socket": to_socket.name + }) + except Exception as e: + raise Exception(f"Error creating link: {str(e)}") + + return created_links - try: - if socket_key in geometry_modifier: - geometry_modifier[socket_key] = socket["value"] - print( - f"Successfully set parameter {socket_name} ({socket_key}) = {socket['value']}") - else: - print(f"Warning: {socket_key} does not exist in modifier") - except Exception as e: - print(f"Error when setting modifier parameter {socket_key}: {str(e)}") - else: - print(f"Warning: Could not find socket named '{socket_name}' in NodeGroupInput") - - # Output all available Socket keys for debugging - available_sockets = [key for key in geometry_modifier.keys() if key.startswith("Socket_")] - if available_sockets: - print(f"Available Socket keys in modifier: {available_sockets}") + def _set_modifier_parameters(self, geometry_modifier, node_group, input_sockets): + """Set modifier parameters from input socket values""" + try: + group_input_node = next((node for node in node_group.nodes if node.type == 'GROUP_INPUT'), None) + if not group_input_node: + group_input_node = node_group.nodes.new('NodeGroupInput') - except Exception as e: - print(f"Error setting modifier parameters: {str(e)}") - # Fall back to compatibility mode - try: - # Directly iterate through modifier properties and set values - for socket in input_sockets: - if "value" in socket and socket.get("type") != "NodeSocketGeometry": - socket_name = socket.get("name") - # Try to find matching socket keys - for key in geometry_modifier.keys(): - if key.startswith("Socket_"): - # Try to set directly - try: - geometry_modifier[key] = socket["value"] - print(f"Directly set {key} = {socket['value']}") - break - except: - pass - except: - pass + socket_dict = {output.name: i for i, output in enumerate(group_input_node.outputs)} - return { - "status": "success", - "object_name": object_name, - "modifier_name": geometry_modifier.name, - "node_group_name": node_group.name, - "nodes": created_nodes, - "links": created_links - } + for socket in input_sockets: + if "value" in socket and socket.get("type") != "NodeSocketGeometry": + socket_name = socket.get("name") + if socket_name in socket_dict: + socket_key = f"Socket_{socket_dict[socket_name]}" + try: + if socket_key in geometry_modifier: + geometry_modifier[socket_key] = socket["value"] + except Exception as e: + print(f"Error setting modifier parameter {socket_key}: {e}") except Exception as e: - return {"error": f"Error completing geometry node network: {str(e)}"} + print(f"Error setting modifier parameters: {e}") def _create_geometry_nodes_object(self, object_name): """Create a base object and add a geometry nodes modifier @@ -2845,90 +2760,25 @@ def _setup_node_group_interface(self, node_group, input_sockets=None, output_soc # region GeometryNodeInfo def get_node_info(self, output_format='text', include_details=False, node_type_name=None): - """Get node type information - - Args: - output_format: Output format ('text', 'json') - include_details: Whether to include detailed information (properties and sockets) - node_type_name: Node type name, can be a single string, comma-separated multiple node names, or a list of strings - - Returns: - str: Returns node information in different formats based on output_format: - - 'text': String, each line as "TypeName:Description" and detailed information (if include_details is True) - - 'json': JSON formatted string - """ - # Handle include_details type conversion + """Get node type information""" try: - if isinstance(include_details, str): - include_details_lower = include_details.lower() - include_details = include_details_lower == 'true' or include_details_lower == 'yes' or include_details_lower == '1' - - # Handle node_type_name parameter, ensuring it's a list - node_type_names = [] - if node_type_name is not None: - # If it's a string, check if it's a comma-separated list - if isinstance(node_type_name, str): - # Split by comma and strip whitespace - node_type_names = [name.strip() for name in node_type_name.split(',') if name.strip()] - # If it's already a list, use it directly - elif isinstance(node_type_name, list): - node_type_names = node_type_name - else: - raise ValueError(f"node_type_name must be a string or a list, received: {type(node_type_name)}") - - # Ensure all elements in the list are strings - if not all(isinstance(item, str) for item in node_type_names): - raise ValueError(f"All elements in node_type_name list must be strings") - - print(f"Processing node type names: {node_type_names} (from: {node_type_name})") - - # Handle output_format parameter + include_details = self._parse_bool_param(include_details) + node_type_names = self._parse_node_type_names(node_type_name) + if output_format not in ('text', 'json'): raise ValueError(f"Unsupported output format: {output_format}, please use 'text' or 'json'") self._register_node_info_cache() - - # Get all node information node_infos = self._get_nodes_from_cache_or_collect() - - # If specific node type names are specified, return only those nodes' information + if node_type_names: - # Find target nodes target_nodes = [node for node in node_infos if node.name in node_type_names] - - # If no target nodes are found if not target_nodes: - if len(node_type_names) == 1: - return f"Node {node_type_names[0]} does not exist or cannot be created" - else: - return f"Specified nodes do not exist or cannot be created: {', '.join(node_type_names)}" + missing = ', '.join(node_type_names) + return f"Node{'s' if len(node_type_names) > 1 else ''} {missing} do{'es' if len(node_type_names) == 1 else ''} not exist or cannot be created" + node_infos = target_nodes - # Return node information based on output_format - if output_format == 'text': - if len(target_nodes) == 1: - return self._format_single_node_text(target_nodes[0], include_details) - else: - if include_details: - return "\n\n".join(self._format_node_text(node, True) for node in target_nodes) - else: - return '\n'.join(self._format_node_text(node, False) for node in target_nodes) - else: # 'json' - node_dicts = [self._node_to_dict(node, include_details) for node in target_nodes] - if len(node_dicts) == 1: - return json.dumps(node_dicts[0], ensure_ascii=False, indent=2, default=str) - else: - return json.dumps(node_dicts, ensure_ascii=False, indent=2, default=str) - - # Return information about all nodes based on output_format - if output_format == 'text': - if include_details: - return "\n\n".join(self._format_node_text(node, True) for node in node_infos) - else: - return '\n'.join(self._format_node_text(node, False) for node in node_infos) - - else: # 'json' - node_dicts = [self._node_to_dict(node, include_details) for node in node_infos] - return json.dumps(node_dicts, ensure_ascii=False, indent=2, default=str) + return self._format_output(node_infos, output_format, include_details) except Exception as e: print(f"Error getting node information: {str(e)}") @@ -2936,6 +2786,39 @@ def get_node_info(self, output_format='text', include_details=False, node_type_n traceback.print_exc() return f"Error getting node information: {str(e)}" + def _parse_bool_param(self, param): + """Parse boolean parameter from string or bool""" + if isinstance(param, str): + return param.lower() in ('true', 'yes', '1') + return bool(param) + + def _parse_node_type_names(self, node_type_name): + """Parse node type names parameter""" + if node_type_name is None: + return [] + + if isinstance(node_type_name, str): + return [name.strip() for name in node_type_name.split(',') if name.strip()] + elif isinstance(node_type_name, list): + if not all(isinstance(item, str) for item in node_type_name): + raise ValueError("All elements in node_type_name list must be strings") + return node_type_name + else: + raise ValueError(f"node_type_name must be a string or a list, received: {type(node_type_name)}") + + def _format_output(self, node_infos, output_format, include_details): + """Format node information output""" + if output_format == 'json': + node_dicts = [self._node_to_dict(node, include_details) for node in node_infos] + result = node_dicts[0] if len(node_dicts) == 1 else node_dicts + return json.dumps(result, ensure_ascii=False, indent=2, default=str) + else: # text + if len(node_infos) == 1: + return self._format_single_node_text(node_infos[0], include_details) + else: + separator = "\n\n" if include_details else "\n" + return separator.join(self._format_node_text(node, include_details) for node in node_infos) + def _register_node_info_cache(self): """Register custom properties""" if not hasattr(bpy.types.WindowManager, "node_info_cache"): @@ -2946,97 +2829,65 @@ def _register_node_info_cache(self): ) def _collect_socket_info(self, node, is_input: bool = True) -> List[SocketInfo]: - """Collect node socket information - - Args: - node: Node object - is_input: Whether the socket is an input socket - - Returns: - List[SocketInfo]: List of socket information - """ - sockets = [] + """Collect node socket information""" socket_list = node.inputs if is_input else node.outputs + sockets = [] for socket in socket_list: - # Default value handling - default_value = None - if hasattr(socket, 'default_value'): - try: - # Handling different types of default values - if hasattr(socket.default_value, '__len__'): - # Vectors, colors, etc. - default_value = list(socket.default_value) - else: - # Scalar values - default_value = socket.default_value - except: - default_value = None - - # Create SocketInfo object - socket_info = SocketInfo( + default_value = self._get_socket_default_value(socket) + sockets.append(SocketInfo( name=socket.name, type=socket.type, description=socket.description, - identifier='', enabled=socket.enabled, hide=socket.hide, default_value=default_value - ) - - sockets.append(socket_info) + )) return sockets - def _collect_property_info(self, node_type) -> List[PropertyInfo]: - """Collect node property information - - Args: - node_type: Node type + def _get_socket_default_value(self, socket): + """Get socket default value safely""" + if not hasattr(socket, 'default_value'): + return None + try: + if hasattr(socket.default_value, '__len__'): + return list(socket.default_value) + else: + return socket.default_value + except: + return None - Returns: - List[PropertyInfo]: List of property information - """ + def _collect_property_info(self, node_type) -> List[PropertyInfo]: + """Collect node property information""" properties = [] + excluded_props = {'rna_type', 'name', 'location', 'width', 'width_hidden', + 'height', 'dimensions', 'inputs', 'outputs', 'internal_links'} + + parent_props = {prop.identifier for base in node_type.__bases__ + for prop in base.bl_rna.properties} - # Get parent class identifier, to exclude inherited properties - parent_props = [prop.identifier for base in node_type.__bases__ - for prop in base.bl_rna.properties] - - # Iterate over node type's properties for prop in node_type.bl_rna.properties: - # Skip inherited properties and built-in basic properties - if (prop.identifier in parent_props or - prop.identifier in ['rna_type', 'name', 'location', 'width', - 'width_hidden', 'height', 'dimensions', - 'inputs', 'outputs', 'internal_links']): + if prop.identifier in parent_props or prop.identifier in excluded_props: continue - # Get default value (based on property type) - default_value = None enum_items = [] - - # Handling different types of properties + default_value = None + if prop.type == 'ENUM': - # Handling enum types - enum_items = [{'identifier': item.identifier, - 'name': item.name, - 'description': item.description} - for item in prop.enum_items] + enum_items = [{'identifier': item.identifier, 'name': item.name, + 'description': item.description} for item in prop.enum_items] elif hasattr(prop, 'default'): default_value = prop.default - # Create PropertyInfo object - prop_info = PropertyInfo( + properties.append(PropertyInfo( identifier=prop.identifier, name=prop.name, description=prop.description, type=prop.type, default_value=default_value, enum_items=enum_items - ) - - properties.append(prop_info) + )) return properties @@ -3172,31 +3023,23 @@ def _get_nodes_from_cache_or_collect(self) -> List[NodeInfo]: self._update_cache(node_infos) return node_infos - def _format_socket_info_text(self, socket: SocketInfo, index: int, indent: str = " ") -> List[str]: - """Format socket information as a list of text lines""" - lines = [f"{indent}{index}. {socket.name} ({socket.type})"] - if socket.description: - lines.append(f"{indent} Description: {socket.description}") - if socket.default_value is not None: - lines.append(f"{indent} Default value: {socket.default_value}") - return lines - - def _format_property_info_text(self, prop: PropertyInfo, index: int, indent: str = " ") -> List[str]: - """Format property information as a list of text lines""" - lines = [f"{indent}{index}. {prop.name} ({prop.type})"] - if prop.description: - lines.append(f"{indent} Description: {prop.description}") - - if prop.default_value is not None: - lines.append(f"{indent} Default value: {prop.default_value}") - - if prop.enum_items: + def _format_item_text(self, item, index: int, indent: str = " ") -> List[str]: + """Format socket or property information as text lines""" + lines = [f"{indent}{index}. {item.name} ({item.type})"] + + if hasattr(item, 'description') and item.description: + lines.append(f"{indent} Description: {item.description}") + + if hasattr(item, 'default_value') and item.default_value is not None: + lines.append(f"{indent} Default value: {item.default_value}") + + if hasattr(item, 'enum_items') and item.enum_items: lines.append(f"{indent} Options:") - for i, item in enumerate(prop.enum_items): - lines.append(f"{indent} {i}. {item['name']} ('{item['identifier']}')") - if item['description']: - lines.append(f"{indent} Description: {item['description']}") - + for i, enum_item in enumerate(item.enum_items): + lines.append(f"{indent} {i}. {enum_item['name']} ('{enum_item['identifier']}')") + if enum_item['description']: + lines.append(f"{indent} Description: {enum_item['description']}") + return lines def _format_node_text(self, node: NodeInfo, include_details: bool = False) -> str: @@ -3205,24 +3048,14 @@ def _format_node_text(self, node: NodeInfo, include_details: bool = False) -> st return f"{node.name}:{node.description}" lines = [f"{node.name}:{node.description}"] - - # Add property information - if node.properties: - lines.append(" Properties:") - for i, prop in enumerate(node.properties): - lines.extend(self._format_property_info_text(prop, i, " ")) - - # Add input socket information - if node.inputs: - lines.append(" Input sockets:") - for i, socket in enumerate(node.inputs): - lines.extend(self._format_socket_info_text(socket, i, " ")) - - # Add output socket information - if node.outputs: - lines.append(" Output sockets:") - for i, socket in enumerate(node.outputs): - lines.extend(self._format_socket_info_text(socket, i, " ")) + + for section_name, items in [("Properties", node.properties), + ("Input sockets", node.inputs), + ("Output sockets", node.outputs)]: + if items: + lines.append(f" {section_name}:") + for i, item in enumerate(items): + lines.extend(self._format_item_text(item, i, " ")) return "\n".join(lines) @@ -3231,39 +3064,23 @@ def _format_single_node_text(self, node: NodeInfo, include_details: bool = False lines = [f"Node: {node.name}", f"Description: {node.description}"] if include_details: - # Add property information - if node.properties: - lines.append("\nProperties:") - for i, prop in enumerate(node.properties): - lines.append(f" {i}. {prop.name} ({prop.type})") - if prop.description: - lines.append(f" Description: {prop.description}") - if prop.default_value is not None: - lines.append(f" Default value: {prop.default_value}") - if prop.enum_items: - lines.append(f" Options:") - for j, item in enumerate(prop.enum_items): - lines.append(f" {j}. {item['name']} ('{item['identifier']}')") - if item['description']: - lines.append(f" Description: {item['description']}") - - # Add input socket information - if node.inputs: - lines.append("\nInput sockets:") - for i, socket in enumerate(node.inputs): - lines.append(f" {i}. {socket.name} ({socket.type})") - if socket.description: - lines.append(f" Description: {socket.description}") - if socket.default_value is not None: - lines.append(f" Default value: {socket.default_value}") - - # Add output socket information - if node.outputs: - lines.append("\nOutput sockets:") - for i, socket in enumerate(node.outputs): - lines.append(f" {i}. {socket.name} ({socket.type})") - if socket.description: - lines.append(f" Description: {socket.description}") + for section_name, items in [("Properties", node.properties), + ("Input sockets", node.inputs), + ("Output sockets", node.outputs)]: + if items: + lines.append(f"\n{section_name}:") + for i, item in enumerate(items): + lines.append(f" {i}. {item.name} ({item.type})") + if hasattr(item, 'description') and item.description: + lines.append(f" Description: {item.description}") + if hasattr(item, 'default_value') and item.default_value is not None: + lines.append(f" Default value: {item.default_value}") + if hasattr(item, 'enum_items') and item.enum_items: + lines.append(" Options:") + for j, enum_item in enumerate(item.enum_items): + lines.append(f" {j}. {enum_item['name']} ('{enum_item['identifier']}')") + if enum_item['description']: + lines.append(f" Description: {enum_item['description']}") return '\n'.join(lines)