diff --git a/blroots/__init__.py b/blroots/__init__.py
index 32208d3..fb588e1 100644
--- a/blroots/__init__.py
+++ b/blroots/__init__.py
@@ -5,17 +5,17 @@ from .gather import BLExtInfo, blext_info
from .utils import bl_logger as log
__all__ = [
+ 'BLExtInfo',
+ 'BLField',
'bl_prop',
+ 'blext_dir',
+ 'blext_info',
+ 'blext_prefs',
'gather',
'init',
+ 'log',
+ 'log',
'prefs',
'registration',
'typ',
- 'blext_dir',
- 'blext_prefs',
- 'BLField',
- 'BLExtInfo',
- 'log',
- 'blext_info',
- 'bl_logger',
]
diff --git a/blroots/bl_prop/__init__.py b/blroots/bl_prop/__init__.py
index 7ada836..7efd746 100644
--- a/blroots/bl_prop/__init__.py
+++ b/blroots/bl_prop/__init__.py
@@ -5,10 +5,10 @@ from .bl_prop import BLProp, SupportedBLPropType
from .signal import Signal
__all__ = [
- 'types',
'BLField',
'BLIDProps',
'BLProp',
- 'SupportedBLPropType',
'Signal',
+ 'SupportedBLPropType',
+ 'types',
]
diff --git a/blroots/bl_prop/bl_field.py b/blroots/bl_prop/bl_field.py
index 7b962b5..093037a 100644
--- a/blroots/bl_prop/bl_field.py
+++ b/blroots/bl_prop/bl_field.py
@@ -23,12 +23,9 @@ In particular,
import functools
import typing as typ
-import bpy
-
# import griffe
from .bl_id_props import BLIDProps
from .bl_prop import BLProp, SupportedBLPropType
-from .signal import Signal
####################
@@ -44,7 +41,7 @@ class BLField:
display_name: str | None = None,
description: str | None = None,
**bl_prop_info: dict[str, typ.Any],
- ):
+ ) -> None:
"""Initializes and sets the attribute to a given default value.
The attribute **must** declare a type annotation, and it **must** match the type of `default_value`.
@@ -63,35 +60,6 @@ class BLField:
####################
# - Initialization
####################
- def init_on_bl_class(self, bl_class: type[SupportedBLPropType]) -> None:
- # Add 'draw_blfield' Method
- ## - This enables the use of BLProp-specific draw() methods.
- if not hasattr(bl_class, f'blfield__{self.name}'):
- setattr(
- bl_class,
- f'blfield__{self.name}',
- property(lambda bl_instance: self),
- )
- if not hasattr(bl_class, 'draw_blfield'):
-
- def draw_blfield(
- bl_instance: SupportedBLPropType,
- layout: bpy.types.UILayout,
- bl_field_name: str,
- **kwargs: dict[str, typ.Any],
- ) -> None:
- # Find BLField Descriptor on BLInstance
- bl_field = getattr(bl_instance, f'blfield__{bl_field_name}', None)
-
- # Call draw() of BLProp
- if bl_field is not None:
- bl_field.bl_prop.draw(bl_instance, layout, **kwargs)
- else:
- msg = f"A BLField with the name '{bl_field_name}' could not be found on the BLInstance '{bl_instance!s}'"
- raise ValueError(msg)
-
- bl_class.draw_blfield = draw_blfield
-
def __set_name__(self, owner: type[SupportedBLPropType], name: str) -> None:
"""Sets up this descriptor on the class, preparing it for per-instance use.
@@ -106,8 +74,10 @@ class BLField:
owner: The class that contains an attribute assigned to an instance of this descriptor.
name: The name of the attribute that an instance of descriptor was assigned to.
"""
- # Parse BLProp Information
- ## - self.bl_prop is only available after this is done.
+ ####################
+ # - Parse Information for BLProp
+ ####################
+ ## *self.bl_prop is only valid to use after this section runs.
if issubclass(
owner,
BLIDProps | SupportedBLPropType,
@@ -139,12 +109,13 @@ class BLField:
msg = f"Cannot declare 'BLField' (name='{name}') on unsupported class '{owner}'."
raise TypeError(msg)
- # Initialize BLProp on Owner
- ## - Don't initialize a BLField on BLIDProps, since it modifies a global bpy type.
- ## - Instead, its register() method must do the same sequence.
+ ####################
+ # - Initialize Owner
+ ####################
+ ## This is only done if the owner should directly accessed at this point.
+ ## Ex. BLIDProps has its own register() method.
if issubclass(owner, SupportedBLPropType):
self.bl_prop.init_on_bl_class(owner)
- self.init_on_bl_class(owner)
@functools.cached_property
def bl_prop(self) -> BLProp:
@@ -194,27 +165,7 @@ class BLField:
bl_instance: Instance that is accessing the attribute.
owner: The class that owns the instance.
"""
- # Read the Non-Persistent Cached Value
- ## - The value must exist in the thread-safe memory cache.
- cached_value = self.bl_prop.read_nonpersist(bl_instance)
-
- # Check Non-Persistent Cache Status
- ## - Signals are guaranteed not to overlap with any other object.
- if cached_value is Signal.CacheNotReady or cached_value is Signal.CacheEmpty:
- # Check if Blender Class Instance Exists
- if bl_instance is not None:
- # Read Persisted Value from Blender Class Instance
- ## - The value on the BLInstance can persist across runs.
- persisted_value = self.bl_prop.read_persist(bl_instance)
-
- # Fill Non-Persistent Cache
- ## - The next read will be from a thread-safe memory cache.
- ## - This aggressive caching minimizes traffic on `bpy`.
- self.bl_prop.write_nonpersist(bl_instance, persisted_value)
-
- return persisted_value
- return self.bl_prop.default_value ## TODO: Good idea?
- return cached_value
+ return self.bl_prop.read(bl_instance)
def __set__(self, bl_instance: SupportedBLPropType | None, value: typ.Any) -> None:
"""Sets the value described by the BLField.
@@ -230,23 +181,4 @@ class BLField:
bl_instance: Instance that is accessing the attribute.
owner: The class that owns the instance.
"""
- match value:
- # Signal: Invalidate Non-Persistent Cache
- case Signal.InvalidateCache:
- self.bl_prop.invalidate_nonpersist(bl_instance)
-
- # Signal: BLProp-Specific
- case (
- signal
- ) if value is Signal.ResetEnumItems or value is Signal.ResetStrSearch:
- self.bl_prop.handle_signal(signal)
-
- # Signal: Invalid
- case Signal.CacheNotReady | Signal.CacheEmpty:
- msg = 'Cannot set BLField to `CacheEmpty` or `CacheNotReady` signal.'
- raise ValueError(msg)
-
- # General Case:
- case _:
- # Invalidate Persistent AND Non-Persistent Cache
- self.bl_prop.write_persist(bl_instance, value)
+ self.bl_prop.write(bl_instance, value)
diff --git a/blroots/bl_prop/bl_prop.py b/blroots/bl_prop/bl_prop.py
index 31c0c54..6ddf006 100644
--- a/blroots/bl_prop/bl_prop.py
+++ b/blroots/bl_prop/bl_prop.py
@@ -32,9 +32,11 @@ SupportedBLPropType: typ.TypeAlias = (
)
-####################
-# - Blender Property (Abstraction)
-####################
+def bl_prop_instance_property_name(bl_prop_name: str) -> str:
+ """Deduces the internal attribute name where a reference to this class instance will be persisted."""
+ return f'blprop_instance__{bl_prop_name}'
+
+
class BLProp(pyd.BaseModel, frozen=True):
"""Convenient, fast, and safe interface to a property on a Blender class.
@@ -76,7 +78,9 @@ class BLProp(pyd.BaseModel, frozen=True):
default_value: typ.Any
- # TODO: Support certain property flags universally.
+ use_property_group: bool = False
+
+ # TODO: Support certain property flags universally?
####################
# - Computed
@@ -86,70 +90,10 @@ class BLProp(pyd.BaseModel, frozen=True):
"""Deduces the internal attribute name where the raw Blender property data will be persisted."""
return f'blprop__{self.name}'
- ####################
- # - Type Support
- ####################
- @classmethod
- def supports_type_hint(cls, field_type_hint: type) -> bool:
- """Cheaply deduce whether this BLProp implements the given type."""
- raise NotImplementedError
-
- ####################
- # - bpy.props Interface
- ####################
- @property
- def bpy_prop_type(self) -> type[bpy.props.FloatProperty]:
- """The underlying `bpy.props` class backing this property."""
- raise NotImplementedError
-
- @property
- def bpy_prop_kwargs(self) -> dict[str, typ.Any]:
- """Keyword arguments to pass to `self.bpy_prop_type`."""
- raise NotImplementedError
-
- ####################
- # - Encode/Decode
- ####################
- def encode(self, value: typ.Any) -> typ.Any:
- """Encode a value for compatibility with this Blender property, using the encapsulated types."""
- raise NotImplementedError
-
- def decode(self, value: typ.Any) -> typ.Any:
- """Encode a value for compatibility with this Blender property, using the encapsulated types."""
- raise NotImplementedError
-
- def handle_signal(self, signal: Signal) -> None:
- """Handle a signal in a manner specific to a Blender property."""
- raise NotImplementedError
-
- ####################
- # - UI
- ####################
- def draw(
- self,
- bl_instance: SupportedBLPropType,
- layout: bpy.types.UILayout,
- ) -> None:
- """Draw this property to the given Blender `UILayout`.
-
- Notes:
- Can and should be overridden by subclasses.
- Consider this a sensible default.
- """
- ## TODO:
- ## - Universal support for icons?
- layout.prop(bl_instance, self.bl_name, text=self.display_name)
-
- ####################
- # - Selection
- ####################
- @classmethod
- def priority(cls) -> int:
- """The priority with which a type hint will resolve to this implementation in case of a conflict.
-
- Lower implies higher priority.
- """
- raise NotImplementedError
+ @functools.cached_property
+ def instance_bl_name(self):
+ """Deduces the internal attribute name where a reference to this class instance will be persisted."""
+ return bl_prop_instance_property_name(self.name)
####################
# - Creation
@@ -207,6 +151,7 @@ class BLProp(pyd.BaseModel, frozen=True):
self,
bl_class: type[SupportedBLPropType],
) -> None:
+ """Prepare a Blender class to use this BLField"""
"""Declare the Blender property on a Blender class, ensuring that the property will be available to all `SupportedBLPropType` respecting instances of that class.
Parameters:
@@ -214,22 +159,34 @@ class BLProp(pyd.BaseModel, frozen=True):
**Must** be chosen such that `BLPropType.from_type(obj_type) == self`.
"""
- # Add Method: Reset Instance ID
- def reset_instance_id(bl_instance: SupportedBLPropType) -> None:
- bl_instance.instance_id = str(uuid.uuid4())
+ ####################
+ # - Add Method: reset_instance_id
+ ####################
+ if not hasattr(bl_class, 'reset_instance_id'):
- bl_class.reset_instance_id = reset_instance_id
+ def reset_instance_id(bl_instance: SupportedBLPropType) -> None:
+ bl_instance.instance_id = str(uuid.uuid4())
- # ID: Dynamic Assignment to Class Variable
+ bl_class.reset_instance_id = reset_instance_id
+
+ ####################
+ # - [bpy.types.ID] Add Properties: instance_id & self.bpy_prop
+ ####################
if issubclass(bl_class, bpy.types.ID):
# Set Instance ID
if not hasattr(bl_class, 'instance_id'):
bl_class.instance_id = bpy.props.StringProperty()
# Set Property
- setattr(bl_class, self.bl_name, self.bpy_prop)
+ if not hasattr(bl_class, self.bl_name):
+ setattr(bl_class, self.bl_name, self.bpy_prop)
+ else:
+ msg = f'Tried to set attribute {self.bl_name} on `bl_class={bl_class}`, but the attribute already exists.'
+ raise RuntimeError(msg)
- # bpy_struct: Static Assignment to Class Annotations
+ ####################
+ # - [bpy_struct] Add Properties: instance_id & self.bpy_prop
+ ####################
elif issubclass(
bl_class,
bpy.types.AddonPreferences
@@ -244,8 +201,116 @@ class BLProp(pyd.BaseModel, frozen=True):
# Set Annotation
bl_class.__annotations__[self.bl_name] = self.bpy_prop
+ ####################
+ # - Add Attribute: {self.class_bl_name}
+ ####################
+ if not hasattr(bl_class, self.instance_bl_name):
+ setattr(
+ bl_class,
+ self.instance_bl_name,
+ classmethod(lambda _bl_class: self),
+ )
+ ## This may look a little funky. That's because it is a little funky.
+ ## This trick lets gets the actual 'BLProp' instance 'self' from a 'bl_instance'
+ ## 'self' gets bound within the lambda returned by the classmethod.
+ ## In this way, we can access the BLProp underlying any BLField.
+ else:
+ msg = 'Cannot register two identically named BLProps on the same BLClass.'
+ raise ValueError(msg)
+
+ ####################
+ # - Add Method: draw_blprop
+ ####################
+ if not hasattr(bl_class, 'draw_blprop'):
+
+ def draw_blprop(
+ bl_instance: SupportedBLPropType,
+ layout: bpy.types.UILayout,
+ bl_prop_name: str,
+ **kwargs: dict[str, typ.Any],
+ ) -> None:
+ """Find a `BLProp` defined on a Blender instance, and call its `.draw()` method.
+
+ Parameters:
+ bl_instance: An instance of a Blender class, which has been given a `BLProp`.
+ Since this is a method, this takes the place of `self`; that is to say, calling `self.draw_blprop` will set this parameter.
+ layout: A Blender layout to draw to.
+ class_bl_name: A Blender layout to draw to.
+ """
+ # Find 'self' on BLInstance
+ ## The same draw_blprop must work for ALL BLProps with the same logic.
+ ## Thus, 'self.*' would absolutely not work.
+ bl_prop = getattr(
+ bl_instance, bl_prop_instance_property_name(bl_prop_name), None
+ )
+ if bl_prop is not None and isinstance(bl_prop, BLProp):
+ bl_prop.draw(bl_instance, layout, **kwargs)
+ else:
+ msg = f"A BLProp instance could not be found at '{bl_prop_name}' on the BLInstance: '{bl_instance!s}'"
+ raise ValueError(msg)
+
+ bl_class.draw_blprop = draw_blprop
+
####################
- # - Instance Methods
+ # - High-Level Cache Semantics
+ ####################
+ def read(self, bl_instance: SupportedBLPropType | None) -> typ.Any:
+ """A sensible choice of cache semantics for retrieving fields described by this `BLProp`.
+
+ Parameters:
+ bl_instance: Instance that is accessing the attribute.
+ """
+ # Read the Non-Persistent Cached Value
+ ## - The value must exist in the thread-safe memory cache.
+ cached_value = self.read_nonpersist(bl_instance)
+
+ # Check Non-Persistent Cache Status
+ ## - Signals are guaranteed not to overlap with any other object.
+ if cached_value is Signal.CacheNotReady or cached_value is Signal.CacheEmpty:
+ # Check if Blender Class Instance Exists
+ if bl_instance is not None:
+ # Read Persisted Value from Blender Class Instance
+ ## - The value on the BLInstance can persist across runs.
+ persisted_value = self.read_persist(bl_instance)
+
+ # Fill Non-Persistent Cache
+ ## - The next read will be from a thread-safe memory cache.
+ ## - This aggressive caching minimizes traffic on `bpy`.
+ self.write_nonpersist(bl_instance, persisted_value)
+
+ return persisted_value
+ return self.default_value ## TODO: Good idea?
+ return cached_value
+
+ def write(self, bl_instance: SupportedBLPropType | None, value: typ.Any) -> None:
+ """A sensible choice of cache semantics for writing fields described by this `BLProp`.
+
+ Parameters:
+ bl_instance: Instance that is accessing the attribute.
+ """
+ match value:
+ # Signal: Invalidate Non-Persistent Cache
+ case Signal.InvalidateCache:
+ self.invalidate_nonpersist(bl_instance)
+
+ # Signal: BLProp-Specific
+ case signal if (
+ value is Signal.ResetEnumItems or value is Signal.ResetStrSearch
+ ):
+ self.handle_signal(signal)
+
+ # Signal: Invalid
+ case Signal.CacheNotReady | Signal.CacheEmpty:
+ msg = 'Cannot set BLProp to `CacheEmpty` or `CacheNotReady` signal.'
+ raise ValueError(msg)
+
+ # General Case:
+ case _:
+ # Invalidate Persistent AND Non-Persistent Cache
+ self.write_persist(bl_instance, value)
+
+ ####################
+ # - Low-Level Cache Semantics
####################
def read_nonpersist(self, bl_instance: SupportedBLPropType | None) -> typ.Any:
"""Read the non-persistent cache value for this property.
@@ -336,18 +401,34 @@ class BLProp(pyd.BaseModel, frozen=True):
bl_instance: The Blender object to
**NOTE**: `bl_instance` must not be `None`, as neighboring methods sometimes allow.
"""
- signal = bl_prop_cache.write(
- bl_instance,
- key=self.bl_name,
- value=self.encode(value),
- use_nonpersist=False,
- use_persist=True,
- )
+ if self.use_property_group:
+ for subkey, subvalue in value.items():
+ signal = bl_prop_cache.write(
+ getattr(bl_instance, self.bl_name),
+ key=subkey,
+ value=subvalue,
+ use_nonpersist=False,
+ use_persist=True,
+ )
- if signal is Signal.CacheNotReady:
- msg = f"Tried to write value '{self.bl_name}={value}' to persistent cache of bl_instance '{bl_instance}', but the cache was not yet ready."
- raise ValueError(msg)
+ if signal is Signal.CacheNotReady:
+ msg = f"Tried to write value '{self.bl_name}={value}' to persistent cache of bl_instance '{bl_instance}', but the cache was not yet ready."
+ raise ValueError(msg)
+ else:
+ signal = bl_prop_cache.write(
+ bl_instance,
+ key=self.bl_name,
+ value=self.encode(value),
+ use_nonpersist=False,
+ use_persist=True,
+ use_group=self.use_property_group,
+ )
+ if signal is Signal.CacheNotReady:
+ msg = f"Tried to write value '{self.bl_name}={value}' to persistent cache of bl_instance '{bl_instance}', but the cache was not yet ready."
+ raise ValueError(msg)
+
+ # Write Entire Structure to Non-Persistent Cache
self.write_nonpersist(bl_instance, value)
def invalidate_nonpersist(self, bl_instance: SupportedBLPropType | None) -> None:
@@ -361,3 +442,73 @@ class BLProp(pyd.BaseModel, frozen=True):
bl_instance,
self.bl_name,
)
+
+ ####################
+ # - Overridable: UI
+ ####################
+ def draw(
+ self,
+ bl_instance: SupportedBLPropType,
+ layout: bpy.types.UILayout,
+ text: str | None = None,
+ ) -> None:
+ """Draw this property to the given Blender `UILayout`.
+
+ Notes:
+ Can and should be overridden by subclasses.
+ Consider this a sensible default.
+ """
+ ## TODO:
+ ## - Universal support for icons?
+ layout.prop(
+ bl_instance,
+ self.bl_name,
+ text=self.display_name if text is None else text,
+ )
+
+ ####################
+ # - Abstract: Type Hint Support
+ ####################
+ @classmethod
+ def supports_type_hint(cls, field_type_hint: type) -> bool:
+ """Cheaply deduce whether this BLProp implements the given type."""
+ raise NotImplementedError
+
+ ####################
+ # - Abstract: bpy.props Interface
+ ####################
+ @property
+ def bpy_prop_type(self) -> type[bpy.props.FloatProperty]:
+ """The underlying `bpy.props` class backing this property."""
+ raise NotImplementedError
+
+ @property
+ def bpy_prop_kwargs(self) -> dict[str, typ.Any]:
+ """Keyword arguments to pass to `self.bpy_prop_type`."""
+ raise NotImplementedError
+
+ ####################
+ # - Abstract: Encode/Decode
+ ####################
+ def encode(self, value: typ.Any) -> typ.Any:
+ """Encode a value for compatibility with this Blender property, using the encapsulated types."""
+ raise NotImplementedError
+
+ def decode(self, value: typ.Any) -> typ.Any:
+ """Encode a value for compatibility with this Blender property, using the encapsulated types."""
+ raise NotImplementedError
+
+ def handle_signal(self, signal: Signal) -> None:
+ """Handle a signal in a manner specific to a Blender property."""
+ raise NotImplementedError
+
+ ####################
+ # - Selection
+ ####################
+ @classmethod
+ def priority(cls) -> int:
+ """The priority with which a type hint will resolve to this implementation in case of a conflict.
+
+ Lower implies higher priority.
+ """
+ raise NotImplementedError
diff --git a/blroots/bl_prop/types/__init__.py b/blroots/bl_prop/types/__init__.py
index ace3f59..55f6ae1 100644
--- a/blroots/bl_prop/types/__init__.py
+++ b/blroots/bl_prop/types/__init__.py
@@ -1,11 +1,15 @@
from .bool_prop import BoolProp
from .float_prop import FloatProp
from .int_prop import IntProp
+from .path_prop import PathProp
+from .str_enum_prop import StrEnumProp
from .str_prop import StrProp
__all__ = [
'BoolProp',
'FloatProp',
'IntProp',
+ 'PathProp',
+ 'StrEnumProp',
'StrProp',
]
diff --git a/blroots/bl_prop/types/bool_prop.py b/blroots/bl_prop/types/bool_prop.py
index eb5179a..a359a19 100644
--- a/blroots/bl_prop/types/bool_prop.py
+++ b/blroots/bl_prop/types/bool_prop.py
@@ -91,6 +91,7 @@ class BoolProp(BLProp, frozen=True):
self,
bl_instance: SupportedBLPropType,
layout: bpy.types.UILayout,
+ text: str | None = None,
toggle: bool = False,
expand: bool = False,
) -> None:
@@ -98,7 +99,7 @@ class BoolProp(BLProp, frozen=True):
layout.prop(
bl_instance,
self.bl_name,
- text=self.display_name,
+ text=self.display_name if text is None else text,
toggle=toggle,
expand=expand,
)
diff --git a/blroots/bl_prop/types/class_prop.py b/blroots/bl_prop/types/class_prop.py
index f8152cf..e0bc9fa 100644
--- a/blroots/bl_prop/types/class_prop.py
+++ b/blroots/bl_prop/types/class_prop.py
@@ -24,25 +24,62 @@ import bpy
from blroots.utils import bl_instance, logger
-from ..bl_prop import BLProp
+from ..bl_prop import BLProp, bl_prop_instance_property_name
log = logger.get(__name__)
PRIORITY = 11
+## TODO: Main thing, I think, at this point, is to find a way to register those ProeprtyGroups mindfully, then go play with it!
+
+
+@typ.runtime_checkable
+class BLPropClass(typ.Protocol):
+ @classmethod
+ def bl_prop_group(cls) -> type[bpy.types.PropertyGroup]: ...
+
+ ## TODO: The bl_prop_group should be checked for:
+ ## - That it is a pydantic BaseModel.
+ ## - That the BaseModel has frozen=True.
+ ## - That data attributes are identical between the BL_ class and this version.
+ ## - That type annotations on data attributes are identical.
+ ## TODO: USAGE
+ ## - This class can be used freely without a backing BL_ class.
+ ## - This class can also be used to type-annotate a BLField.
+ ## -- The BL_ class must be registered before being assigned to anything.
+ ## -- Since frozen=True, only the entire instance can be replaced at a time.
+ ## --- Underneath, a backing PropertyGroup will be mutated as needed.
+ ## -- bl_events must be propagated to the backing PropertyGroup.
+ ## --- Use bl_event.propagate with source='parent' to get data from the PropertyGroup's parent.
+ ## --- Let's make sure to make it easy for instances of PropertyGroup to get that parent!
+ ## --- We can later do something similar for data pipelines between nodes and sockets.
+
+
+# def to_bl_prop_group(
+# self, bl_prop_group: BL_LoggingPrefs
+# ) -> type[bpy.types.PropertyGroup]:
+# bl_prop_group.use_log_file = self.use_log_file
+# bl_prop_group.log_file_level = self.log_file_level
+# bl_prop_group.log_file_path = self.log_file_path
+#
+# bl_prop_group.use_log_console = self.use_log_console
+# bl_prop_group.log_console_level = self.log_console_level
+#
+# @classmethod
+# def from_bl_prop_group(cls, bl_prop_group: BL_LoggingPrefs) -> typ.Self:
+# return cls(
+# use_log_file=bl_prop_group.use_log_file,
+# log_file_level=bl_prop_group.log_file_level,
+# log_file_path=bl_prop_group.log_file_path,
+# use_log_console=bl_prop_group.use_log_console,
+# log_console_level=bl_prop_group.log_console_level,
+# )
+
class ClassProp(BLProp, frozen=True):
- """A single constrained floating-point number.
+ """A single class; in particular, an appropriately configured `pydantic` BaseModel."""
- Parameters:
- abs_min: The absolute minimum value of the floating point number.
- abs_max: The absolute maximum value of the floating point number.
- soft_min: The lowest value that can be set using the UI slider.
- soft_max: The highest value that can be set using the UI slider.
- float_prec: The number of decimal places displayed in the UI.
- float_step: The size of each mouse-move increment when using the UI slider.
- _Divide this integer by `100` for actual change per increment._
- """
+ use_property_group = True
default_value: None
@@ -51,17 +88,23 @@ class ClassProp(BLProp, frozen=True):
####################
@classmethod
def supports_type_hint(cls, field_type_hint: type) -> bool:
- """Support only the explicit type `float` aka. `builtins.float`."""
- return inspect.isclass(field_type_hint) and hasattr(
- field_type_hint, 'property_group'
+ """Support classes that conform to the `BLPropClass` protocol."""
+ return inspect.isclass(field_type_hint) and isinstance(
+ field_type_hint, BLPropClass
)
@property
def bpy_property_group(self) -> type[bpy.props.PropertyGroup]:
"""Dynamically create a PropertyGroup."""
- ## TODO: We can make it; the trouble is, who registers it, and when?
- ## - Also, there's an underlying class instance
- pass
+ return self.type_hint.bl_prop_group()
+ ## TODO: Who registers it, and when?
+ ## TODO: I don't like having the "two-class" paradigm.
+ ## - On the plus side, "outside classes" can be adapted.
+ ## - On the minus side, it's two things now. Two parameter documentations to parse, etc. .
+ ## - If we did go with an all-in-one, typ.Annotated w/partial BLField constructor?
+ ## - Or somehow get pydantic to understand setting it to a BLField?
+ ## - I honestly don't know what to do exactly...
+ ## - On the other hand hand, look how mindful and simple this method is.
####################
# - bpy.props Interface
@@ -69,7 +112,7 @@ class ClassProp(BLProp, frozen=True):
@property
def bpy_prop_type(self) -> type[bpy.props.PointerProperty]:
"""Use `bpy.props.PointerProperty` to reference a particular pre-made `bpy.types.PropertyGroup` subclass."""
- return bpy.props.PointerProperty # type: ignore[no-any-return]
+ return bpy.props.PointerProperty
@functools.cached_property
def bpy_prop_kwargs(self) -> dict[str, typ.Any]:
@@ -81,17 +124,48 @@ class ClassProp(BLProp, frozen=True):
####################
# - Encode/Decode
####################
- def encode(self, value: typ.Any) -> float:
- """Encode a value that supports `float()` to a Blender float."""
+ def encode(self, value: typ.Any) -> dict[str, typ.Any]:
+ """Encode a value to a dictionary that will be written to a `PropertyGroup`.
+
+ Notes:
+ The pickle is this:
+
+ - On the PropertyGroup, each field is associated with a unique BLProp instance.
+ - During BLProp initialization, a class method is created to allow accessing the BLProp underlying any BLField.
+ - Why do we need to do this? BLProp instance uses encode()/decode() to translate between user-facing and Blender-facing representations of data.
+ - Therefore,for each subproperty, retrieve the `BLProp` instance, then use its `.encode()` method to transform `value` fields down to the Blender representation that will be expected by the `PropertyGroup`.
+ - This naturally provides correct recursion, too, since `.encode()` will simply chain its way all the way down to the bottom.
+
+ """
if isinstance(value, self.type_hint):
- # TODO: Dump the model to a dictionary? How to handle the nested BLProps? Better to delegate somehow to the PropertyGroup?
- pass
+ return {
+ bl_prop_name: (
+ bl_prop := getattr( # noqa: F841
+ self.bpy_property_group,
+ bl_prop_instance_property_name(bl_prop_name),
+ )()
+ ).encode(getattr(value, bl_prop_name))
+ for bl_prop_name in self.bpy_property_group.bl_props
+ }
raise NotImplementedError
- def decode(self, raw_bl_value: float) -> float:
- """Decode a Blender float by doing nothing."""
- return raw_bl_value
+ def decode(self, raw_bl_value: bpy.types.PropertyGroup) -> typ.Any:
+ """Decode the subclass of `bpy.types.PropertyGroup` subclass into the `pydantic.BaseModel` contained within `self.type_hint`.
+
+ Notes:
+ Simply reading the `BLField`s invokes `.decode()` recursively and correctly.
+ Thus, decoding ends up being comparatively simpler than `encode()`.
+ """
+ if isinstance(raw_bl_value, self.bpy_property_group):
+ return self.type_hint(
+ **{
+ bl_prop_name: getattr(raw_bl_value, bl_prop_name)
+ for bl_prop_name in self.bpy_property_group.bl_props
+ }
+ )
+
+ raise NotImplementedError
####################
# - UI
diff --git a/blroots/bl_prop/types/enum_prop.py b/blroots/bl_prop/types/enum_prop.py
deleted file mode 100644
index e69de29..0000000
diff --git a/blroots/bl_prop/types/float_prop.py b/blroots/bl_prop/types/float_prop.py
index 9957c05..abcfacf 100644
--- a/blroots/bl_prop/types/float_prop.py
+++ b/blroots/bl_prop/types/float_prop.py
@@ -122,10 +122,16 @@ class FloatProp(BLProp, frozen=True):
self,
bl_instance: SupportedBLPropType,
layout: bpy.types.UILayout,
+ text: str | None = None,
slider: bool = False,
) -> None:
"""Draw this property to the given Blender `UILayout`."""
- layout.prop(bl_instance, self.bl_name, text=self.display_name, slider=slider)
+ layout.prop(
+ bl_instance,
+ self.bl_name,
+ text=self.display_name if text is None else text,
+ slider=slider,
+ )
####################
# - Selection
diff --git a/blroots/bl_prop/types/int_prop.py b/blroots/bl_prop/types/int_prop.py
index df8e179..c9749b7 100644
--- a/blroots/bl_prop/types/int_prop.py
+++ b/blroots/bl_prop/types/int_prop.py
@@ -111,12 +111,16 @@ class IntProp(BLProp, frozen=True):
self,
bl_instance: SupportedBLPropType,
layout: bpy.types.UILayout,
+ text: str | None = None,
slider: bool = False,
) -> None:
"""Draw this property to the given Blender `UILayout`."""
- ## TODO:
- ## - Support for slider
- layout.prop(bl_instance, self.bl_name, text=self.display_name, slider=slider)
+ layout.prop(
+ bl_instance,
+ self.bl_name,
+ text=self.display_name if text is None else text,
+ slider=slider,
+ )
####################
# - Selection
diff --git a/blroots/bl_prop/types/path_prop.py b/blroots/bl_prop/types/path_prop.py
index e69de29..b05da46 100644
--- a/blroots/bl_prop/types/path_prop.py
+++ b/blroots/bl_prop/types/path_prop.py
@@ -0,0 +1,130 @@
+# blroots
+# Copyright (C) 2025 blroots Project Contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+"""Implements `PathProp`."""
+
+import builtins
+import functools
+import typing as typ
+from pathlib import Path
+
+import bpy
+
+from ..bl_prop import BLProp, SupportedBLPropType
+
+PRIORITY = 20
+
+
+class PathProp(BLProp, frozen=True):
+ """A single constrained floating-point number.
+
+ Parameters:
+ abs_min: The absolute minimum value of the floating point number.
+ abs_max: The absolute maximum value of the floating point number.
+ soft_min: The lowest value that can be set using the UI slider.
+ soft_max: The highest value that can be set using the UI slider.
+ float_prec: The number of decimal places displayed in the UI.
+ float_step: The size of each mouse-move increment when using the UI slider.
+ _Divide this integer by `100` for actual change per increment._
+ """
+
+ default_value: Path
+ ## TODO: Use 'before' field validator to select a default value when None is specified, then an 'after' field validator to constrain it to abs_min and abs_max.
+
+ # Constraints
+ is_file: bool = False
+ is_dir: bool = False
+ ## TODO: Validator that disallows both is_file and is_dir
+
+ ## TODO: A "relative" constraint that uses '//'
+
+ ####################
+ # - Type Support
+ ####################
+ @classmethod
+ def supports_type_hint(cls, field_type_hint: type) -> bool:
+ """Support only the explicit type `float` aka. `builtins.float`."""
+ return field_type_hint is Path
+
+ ####################
+ # - bpy.props Interface
+ ####################
+ @property
+ def bpy_prop_type(self) -> type[bpy.props.StringProperty]:
+ """The underlying `bpy.props` class backing this property."""
+ return bpy.props.StringProperty # type: ignore[no-any-return]
+
+ @functools.cached_property
+ def bpy_prop_kwargs(self) -> dict[str, typ.Any]:
+ """Keyword arguments to pass to `self.bpy_prop_type`."""
+ kwargs: dict[str, typ.Any] = {
+ 'default': self.encode(self.default_value),
+ }
+
+ if self.is_file:
+ kwargs |= {'subtype': 'FILE_PATH'}
+
+ elif self.is_dir:
+ kwargs |= {'subtype': 'DIR_PATH'}
+
+ else:
+ kwargs |= {'subtype': 'FILE_PATH'}
+
+ return kwargs
+
+ ####################
+ # - Encode/Decode
+ ####################
+ def encode(self, value: typ.Any) -> str:
+ """Encode a value that supports `float()` to a Blender float."""
+ # Parse the Value
+ if isinstance(value, Path):
+ parsed_value = str(value.resolve())
+ else:
+ msg = f"Value '{value}' must be a Path."
+ raise TypeError(msg)
+
+ return parsed_value
+
+ def decode(self, raw_bl_value: str) -> Path:
+ """Decode a Blender float by doing nothing."""
+ return Path(bpy.path.abspath(raw_bl_value))
+
+ ####################
+ # - UI
+ ####################
+ def draw(
+ self,
+ bl_instance: SupportedBLPropType,
+ layout: bpy.types.UILayout,
+ text: str | None = None,
+ placeholder: str = '',
+ ) -> None:
+ """Draw this property to the given Blender `UILayout`."""
+ layout.prop(
+ bl_instance,
+ self.bl_name,
+ text=self.display_name if text is None else text,
+ placeholder=placeholder,
+ )
+
+ ####################
+ # - Selection
+ ####################
+ @classmethod
+ def priority(cls) -> int:
+ """Priority with which to prefer this implementation over others compatible with the same type hint."""
+ return PRIORITY
diff --git a/blroots/bl_prop/types/str_enum_prop.py b/blroots/bl_prop/types/str_enum_prop.py
new file mode 100644
index 0000000..b32c423
--- /dev/null
+++ b/blroots/bl_prop/types/str_enum_prop.py
@@ -0,0 +1,123 @@
+# blroots
+# Copyright (C) 2025 blroots Project Contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+"""Implements `StrEnumProp`."""
+
+import enum
+import functools
+import typing as typ
+
+import bpy
+
+from ..bl_prop import BLProp, SupportedBLPropType
+
+PRIORITY = 5
+
+
+class StrEnumProp(BLProp, frozen=True):
+ """A static enumeration."""
+
+ default_value: enum.StrEnum
+ ## TODO: Use 'before' field validator to select a default value when None is specified, then an 'after' field validator to constrain it to abs_min and abs_max.
+
+ ####################
+ # - Type Support
+ ####################
+ @classmethod
+ def supports_type_hint(cls, field_type_hint: type) -> bool:
+ """Support only the explicit type `float` aka. `builtins.float`."""
+ return (
+ issubclass(field_type_hint, enum.StrEnum)
+ and hasattr(field_type_hint, 'bl_display_name')
+ and hasattr(field_type_hint, 'bl_icon')
+ )
+
+ ####################
+ # - bpy.props Interface
+ ####################
+ @property
+ def bpy_prop_type(self) -> type[bpy.props.EnumProperty]:
+ """The underlying `bpy.props` class backing this property."""
+ return bpy.props.EnumProperty # type: ignore[no-any-return]
+
+ @functools.cached_property
+ def bpy_prop_kwargs(self) -> dict[str, typ.Any]:
+ """Keyword arguments to pass to `self.bpy_prop_type`."""
+ kwargs: dict[str, typ.Any] = {
+ 'items': [
+ (
+ el.name,
+ el.bl_display_name,
+ el.bl_display_name,
+ el.bl_icon,
+ idx,
+ )
+ for idx, el in enumerate(self.type_hint)
+ ],
+ 'default': self.encode(self.default_value),
+ ## TODO: Parse Enum.__doc__ to get description from the Attributes section.
+ }
+
+ return kwargs
+
+ ####################
+ # - Encode/Decode
+ ####################
+ def encode(self, value: typ.Any) -> enum.StrEnum:
+ """Encode a value that supports `float()` to a Blender float."""
+ if isinstance(value, self.type_hint):
+ parsed_value = value
+
+ elif isinstance(value, str):
+ parsed_value = getattr(self.type_hint, value, None)
+ if parsed_value is None:
+ msg = f"Object '{value}' could not be coerced to the enum {self.type_hint} during encoding."
+ raise ValueError(msg)
+
+ return parsed_value.name
+
+ def decode(self, raw_bl_value: str) -> str:
+ """Decode a Blender float by doing nothing."""
+ parsed_value = getattr(self.type_hint, raw_bl_value, None)
+ if parsed_value is not None:
+ return raw_bl_value
+
+ msg = f"String '{raw_bl_value}' could not be coerced to the enum {self.type_hint} during decoding."
+ raise ValueError(msg)
+
+ ####################
+ # - UI
+ ####################
+ def draw(
+ self,
+ bl_instance: SupportedBLPropType,
+ layout: bpy.types.UILayout,
+ text: str | None = None,
+ ) -> None:
+ """Draw this property to the given Blender `UILayout`."""
+ layout.prop(
+ bl_instance,
+ self.bl_name,
+ text=self.display_name if text is None else text,
+ )
+
+ ####################
+ # - Selection
+ ####################
+ @classmethod
+ def priority(cls) -> int:
+ """Priority with which to prefer this implementation over others compatible with the same type hint."""
+ return PRIORITY
diff --git a/blroots/bl_prop/types/str_prop.py b/blroots/bl_prop/types/str_prop.py
index faad954..e45f806 100644
--- a/blroots/bl_prop/types/str_prop.py
+++ b/blroots/bl_prop/types/str_prop.py
@@ -28,16 +28,11 @@ PRIORITY = 20
class StrProp(BLProp, frozen=True):
- """A single constrained floating-point number.
+ """A single constrained string.
Parameters:
- abs_min: The absolute minimum value of the floating point number.
- abs_max: The absolute maximum value of the floating point number.
- soft_min: The lowest value that can be set using the UI slider.
- soft_max: The highest value that can be set using the UI slider.
- float_prec: The number of decimal places displayed in the UI.
- float_step: The size of each mouse-move increment when using the UI slider.
- _Divide this integer by `100` for actual change per increment._
+ is_secret: Whether the string should be considered a secret value.
+ Secrets display as dots in the interface, and aren't saved in the `.blend` on save.
"""
default_value: str
@@ -101,11 +96,15 @@ class StrProp(BLProp, frozen=True):
self,
bl_instance: SupportedBLPropType,
layout: bpy.types.UILayout,
+ text: str | None = None,
placeholder: str = '',
) -> None:
"""Draw this property to the given Blender `UILayout`."""
layout.prop(
- bl_instance, self.bl_name, text=self.display_name, placeholder=placeholder
+ bl_instance,
+ self.bl_name,
+ text=self.display_name if text is None else text,
+ placeholder=placeholder,
)
####################
diff --git a/blroots/prefs.py b/blroots/prefs.py
deleted file mode 100644
index 7319fee..0000000
--- a/blroots/prefs.py
+++ /dev/null
@@ -1,164 +0,0 @@
-import logging
-import typing as typ
-from pathlib import Path
-
-import bpy
-
-from blroots import gather
-from blroots.bl_prop import BLField
-from blroots.utils import bl_logger
-
-
-####################
-# - User Preferences Mixin
-####################
-def LoggingPrefs(blext_pkg: str) -> type[type]:
- class _LoggingPrefs(bpy.types.AddonPreferences):
- """Implements the `bl_logger` user preferences integration.
-
- Notes:
- To check for the presence of this integration, use:
- ```python
- getattr(addon_prefs, 'enable_integration__bl_logger', False)
- ```
- """
-
- enable_integration__bl_logger: bool = True
-
- ####################
- # - Properties
- ####################
- # Logging
- ## File Logging
- use_log_file: bool = BLField(
- gather.blext_info(blext_pkg).use_log_file,
- display_name='Log to File',
- )
- # log_file_level: bl_logger.BLExtLogLevel = BLField(
- # gather.blext_info(blext_pkg).log_file_level,
- # display_name='File Log Level',
- # )
- # log_file_path: Path = BLField(
- # gather.blext_info(blext_pkg).log_file_path,
- # display_name='File Log Path',
- # )
-
- ## Console Logging
- use_log_console: bool = BLField(
- gather.blext_info(blext_pkg).use_log_console,
- display_name='Log to Console',
- )
- # log_console_level: bl_logger.BLExtLogLevel = BLField(
- # gather.blext_info(blext_pkg).log_console_level,
- # display_name='Console Log Level',
- # )
-
- ####################
- # - Logging
- ####################
- def conform_logger(self, logger: logging.Logger) -> None:
- """Conform a logger's settings to the extension preferences.
-
- Parameters:
- logger: The logger to configure using settings in the addon preferences.
- """
- bl_logger.update_logger(
- logger,
- cb_console_handler=bl_logger.sensible_console_handler,
- cb_file_handler=bl_logger.sensible_file_handler,
- console_level=(
- self.log_console_level.log_level if self.use_log_console else None
- ),
- file_path=(self.log_file_path if self.use_log_file else None),
- file_level=(
- self.log_file_level.log_level if self.use_log_file else None
- ),
- )
-
- # @bl_event.run_on_event(bl_event.Event.BLClassRegistered)
- # @bl_event.run_on_event(
- # bl_event.Event.PropChanged,
- # on_props={
- # 'use_log_file',
- # 'log_file_level',
- # 'log_file_path',
- # 'use_log_console',
- # 'log_console_level',
- # },
- # )
- def conform_all_loggers_to_prefs(self) -> None:
- """Called to reconfigure all loggers to match newly-altered addon preferences.
-
- This causes ex. changes to desired console log level to immediately be applied, but only the this addon's loggers.
-
- Parameters:
- single_logger_to_setup: When set, only this logger will be setup.
- Otherwise, **all addon loggers will be setup**.
- """
- blext_pkg = self.bl_idname
- for logger in bl_logger.all_blext_loggers(blext_pkg):
- self.setup_logger(logger)
-
- ####################
- # - UI
- ####################
- def draw_logging_prefs(self, layout: bpy.types.UILayout) -> None:
- """Draw the standardized addon preferences into a UILayout.
-
- Examples:
- Run this within the `draw()` method of the addon preferences class like so:
- ```python
- layout = self.layout
- self.draw_standard_prefs(layout)
- ```
-
- Parameters:
- context: The Blender context object.
- """
- ####################
- # - Logging
- ####################
- # Box: Log Level
- box = layout.box()
- row = box.row()
- row.alignment = 'CENTER'
- row.label(text='Logging')
-
- # Split
- split = box.split(factor=0.5)
-
- ## Split Col: Console Logging
- col = split.column()
- row = col.row()
- self.draw_blfield(row, 'use_log_console', toggle=True)
-
- row = col.row()
- row.enabled = self.use_log_console
- # self.draw_blfield(row, 'log_level_console')
-
- ## Split Col: File Logging
- col = split.column()
- row = col.row()
- self.draw_blfield(row, 'use_log_file', toggle=True)
-
- row = col.row()
- row.enabled = self.use_log_file
- # self.draw_blfield(row, 'log_file_path')
-
- row = col.row()
- row.enabled = self.use_log_file
- # self.draw_blfield(row, 'log_level_file')
-
- return _LoggingPrefs
-
-
-def SensiblePrefs(blext_pkg: str) -> type[type]:
- class _SensiblePrefs(LoggingPrefs(blext_pkg)):
- """Sensible set of baseline preferences fields, for use in extensions.
-
- Implements the following `blroots` integration:
-
- - `bl_logger`: Enable the integration of `blroots` logging with extension preferences.
- """
-
- return _SensiblePrefs
diff --git a/blroots/utils/__init__.py b/blroots/utils/__init__.py
index 5ef9514..4bf583a 100644
--- a/blroots/utils/__init__.py
+++ b/blroots/utils/__init__.py
@@ -1,4 +1,4 @@
-# blext
+# blroots
# Copyright (C) 2025 blext Project Contributors
#
# This program is free software: you can redistribute it and/or modify
diff --git a/blroots/utils/bl_logger.py b/blroots/utils/bl_logger.py
index e939220..5d49c19 100644
--- a/blroots/utils/bl_logger.py
+++ b/blroots/utils/bl_logger.py
@@ -23,11 +23,12 @@ import typing as typ
from pathlib import Path
import bpy
+import pydantic as pyd
import rich.console
import rich.logging
import rich.traceback
-from blroots import gather
+from blroots import bl_prop, gather
####################
# - Configuration
@@ -53,12 +54,12 @@ LogLevel: typ.TypeAlias = int
class BLExtLogLevel(enum.StrEnum):
"""Log level of a Blender extension.
- Attributes:
- Debug: Low-level messages for debugging.
- Info: Communications about routine operations.
- Warning: Something that might be wrong.
- Error: Something wrong that can be recovered from.
- Critical: Something wrong that cannot be recovered from.
+ Attributes:
+ Debug: Low-level messages for debugging.
+ Info: Communications about routine operations.
+ Warning: Something that might be wrong.
+ Error: Something wrong that can be recovered from.
+ Critical: Something wrong that cannot be recovered from.
"""
Debug = enum.auto()
@@ -292,3 +293,165 @@ def sensible_file_handler(
file_handler.setFormatter(file_formatter)
file_handler.setLevel(level)
return file_handler
+
+
+####################
+# - Logging Preferences
+####################
+class BL_LoggingPrefs(bpy.types.PropertyGroup):
+ bl_props = frozenset(
+ {
+ 'use_log_file',
+ 'log_file_level',
+ 'log_file_path',
+ 'use_log_console',
+ 'log_console_level',
+ }
+ )
+
+ # File Logging
+ use_log_file: bool = BLField(
+ gather.blext_info(blext_pkg).use_log_file,
+ display_name='Log to File',
+ )
+ log_file_level: BLExtLogLevel = BLField(
+ gather.blext_info(blext_pkg).log_file_level,
+ display_name='File Log Level',
+ )
+ log_file_path: Path = BLField(
+ gather.blext_info(blext_pkg).log_file_path,
+ display_name='File Log Path',
+ )
+
+ # Console Logging
+ use_log_console: bool = BLField(
+ gather.blext_info(blext_pkg).use_log_console,
+ display_name='Log to Console',
+ is_file=True,
+ )
+ log_console_level: BLExtLogLevel = BLField(
+ gather.blext_info(blext_pkg).log_console_level,
+ display_name='Console Log Level',
+ )
+
+
+class LoggingPrefs(pyd.BaseModel, frozen=True):
+ @classmethod
+ def bl_prop_group(cls) -> type[bpy.types.PropertyGroup]:
+ return BL_LoggingPrefs
+
+ bl_idname: str
+
+ # Console Logging
+ use_log_console: bool = gather.blext_info(blext_pkg).use_log_console
+ log_console_level: BLExtLogLevel = gather.blext_info(blext_pkg).log_console_level
+
+ # File Logging
+ use_log_file: bool = gather.blext_info(blext_pkg).use_log_file
+ log_file_level: BLExtLogLevel = gather.blext_info(blext_pkg).log_file_level
+ log_file_path: Path = gather.blext_info(blext_pkg).log_file_path
+
+ # Console Logging
+ use_log_console: bool = gather.blext_info(blext_pkg).use_log_console
+ log_console_level: BLExtLogLevel = gather.blext_info(blext_pkg).log_console_level
+
+ ####################
+ # - Logging
+ ####################
+ def conform_logger(self, logger: logging.Logger) -> None:
+ """Conform a logger's settings to the extension preferences.
+
+ Parameters:
+ logger: The logger to configure using settings in the addon preferences.
+ """
+ update_logger(
+ logger,
+ cb_console_handler=sensible_console_handler,
+ cb_file_handler=sensible_file_handler,
+ console_level=(
+ self.log_console_level.log_level if self.use_log_console else None
+ ),
+ file_path=(self.log_file_path if self.use_log_file else None),
+ file_level=(self.log_file_level.log_level if self.use_log_file else None),
+ )
+
+ @bl_event.run_on_event(bl_event.Event.BLClassRegistered)
+ @bl_event.run_on_event(
+ bl_event.Event.PropChanged,
+ on_props={
+ 'use_log_file',
+ 'log_file_level',
+ 'log_file_path',
+ 'use_log_console',
+ 'log_console_level',
+ },
+ )
+ @bl_event.propagate(source='parent', props={'bl_idname': 'blext_pkg'})
+ def conform_all_loggers_to_prefs(self, blext_pkg: str) -> None:
+ ## TODO: Do some kind of event magic to get the blext_pkg in here frombl_idname of the parent bpy.types.AddonPreferences object containing this class.
+ """Called to reconfigure all loggers to match newly-altered addon preferences.
+
+ This causes ex. changes to desired console log level to immediately be applied, but only the this addon's loggers.
+
+ Parameters:
+ single_logger_to_setup: When set, only this logger will be setup.
+ Otherwise, **all addon loggers will be setup**.
+ """
+ # Retrieve extension __package__ from bl_idname of bpy.types.AddonPreferences
+ for logger in all_blext_loggers(blext_pkg):
+ self.conform_logger(logger)
+
+ ####################
+ # - UI
+ ####################
+ def draw(
+ self,
+ bl_instance: bl_instance.BLInstance,
+ layout: bpy.types.UILayout,
+ _: bpy.types.Context,
+ ) -> None:
+ """Draw the standardized addon preferences into a UILayout.
+
+ Examples:
+ Run this within the `draw()` method of the addon preferences class like so:
+ ```python
+ layout = self.layout
+ self.draw_standard_prefs(layout)
+ ```
+
+ Parameters:
+ context: The Blender context object.
+ """
+ ####################
+ # - Logging
+ ####################
+ # Box: Log Level
+ box = layout.box()
+ row = box.row()
+ row.alignment = 'CENTER'
+ row.label(text='Logging')
+
+ # Split
+ split = box.split(factor=0.5)
+
+ ## Split Col: Console Logging
+ col = split.column()
+ row = col.row()
+ bl_instance.draw_blprop(row, 'use_log_console', toggle=True)
+
+ row = col.row()
+ row.enabled = self.use_log_console
+ bl_instance.draw_blprop(row, 'log_console_level', text='Level')
+
+ ## Split Col: File Logging
+ col = split.column()
+ row = col.row()
+ bl_instance.draw_blprop(row, 'use_log_file', toggle=True)
+
+ row = col.row()
+ row.enabled = self.use_log_file
+ bl_instance.draw_blprop(row, 'log_file_level', text='Level')
+
+ row = col.row()
+ row.enabled = self.use_log_file
+ bl_instance.draw_blprop(row, 'log_file_path', text='')
diff --git a/examples/simple/blroots_simple_example/contracts/operator_types.py b/examples/simple/blroots_simple_example/contracts.py
similarity index 75%
rename from examples/simple/blroots_simple_example/contracts/operator_types.py
rename to examples/simple/blroots_simple_example/contracts.py
index c5471cc..911eafd 100644
--- a/examples/simple/blroots_simple_example/contracts/operator_types.py
+++ b/examples/simple/blroots_simple_example/contracts.py
@@ -1,5 +1,5 @@
-# blext
-# Copyright (C) 2025 blext Project Contributors
+# blroots
+# Copyright (C) 2025 blroots Project Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,9 +13,6 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-
-"""Provides identifiers for Blender operators defined by this addon."""
-
import enum
import blroots as blr
@@ -23,7 +20,17 @@ import blroots as blr
BLEXT_INFO = blr.blext_info(__name__)
+class Icon(enum.StrEnum):
+ """Identifiers for icons used throughout this addon."""
+
+
class OperatorType(enum.StrEnum):
"""Identifiers for addon-defined `bpy.types.Operator`."""
SimpleOperator = BLEXT_INFO.operator_name('simple_operator')
+
+
+class PanelType(enum.StrEnum):
+ """Identifiers for addon-defined `bpy.types.Panel`."""
+
+ SimplePanel = BLEXT_INFO.panel_name('simple_panel')
diff --git a/examples/simple/blroots_simple_example/contracts/__init__.py b/examples/simple/blroots_simple_example/contracts/__init__.py
deleted file mode 100644
index 94347ad..0000000
--- a/examples/simple/blroots_simple_example/contracts/__init__.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# blext
-# Copyright (C) 2025 blext Project Contributors
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-
-"""Independent constants and types, which represent a kind of 'social contract' governing communication between all components of the addon."""
-
-from .icons import Icon
-from .operator_types import (
- OperatorType,
-)
-from .panel_types import (
- PanelType,
-)
-
-__all__ = [
- 'Icon',
- 'OperatorType',
- 'PanelType',
-]
diff --git a/examples/simple/blroots_simple_example/contracts/icons.py b/examples/simple/blroots_simple_example/contracts/icons.py
deleted file mode 100644
index ef2be61..0000000
--- a/examples/simple/blroots_simple_example/contracts/icons.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# blext
-# Copyright (C) 2025 blext Project Contributors
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-
-"""Provides an enum that semantically constrains the use of icons throughout the addon."""
-
-import enum
-
-
-class Icon(enum.StrEnum):
- """Identifiers for icons used throughout this addon."""
diff --git a/examples/simple/blroots_simple_example/contracts/panel_types.py b/examples/simple/blroots_simple_example/contracts/panel_types.py
deleted file mode 100644
index 7f6ec94..0000000
--- a/examples/simple/blroots_simple_example/contracts/panel_types.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# blext
-# Copyright (C) 2025 blext Project Contributors
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-
-"""Provides identifiers for Blender panels defined by this addon."""
-
-import enum
-
-import blroots as blr
-
-BLEXT_INFO = blr.blext_info(__name__)
-
-
-class PanelType(enum.StrEnum):
- """Identifiers for addon-defined `bpy.types.Panel`."""
-
- SimplePanel = BLEXT_INFO.panel_name('simple_panel')
diff --git a/examples/simple/blroots_simple_example/operators/simple_operator.py b/examples/simple/blroots_simple_example/operators/simple_operator.py
index d95b774..098e5c4 100644
--- a/examples/simple/blroots_simple_example/operators/simple_operator.py
+++ b/examples/simple/blroots_simple_example/operators/simple_operator.py
@@ -40,10 +40,9 @@ class SimpleOperator(bpy.types.Operator):
def execute(self, context: bpy.types.Context) -> blr.typ.BLOperatorStatus:
"""Display a simple message on execution."""
- scene = context.scene
self.report(
{'INFO'},
- f'The simple integer was set to {scene.simple_integer}.',
+ f'The simple integer was set to {context.scene.simple_integer}.',
)
return {'FINISHED'}
@@ -53,5 +52,3 @@ class SimpleOperator(bpy.types.Operator):
# - Blender Registration
####################
BL_REGISTER = [SimpleOperator]
-BL_HANDLERS = None
-BL_KEYMAP_ITEMS = None
diff --git a/examples/simple/blroots_simple_example/panels/simple_panel.py b/examples/simple/blroots_simple_example/panels/simple_panel.py
index cb31b0a..25c10a3 100644
--- a/examples/simple/blroots_simple_example/panels/simple_panel.py
+++ b/examples/simple/blroots_simple_example/panels/simple_panel.py
@@ -52,7 +52,7 @@ class SimplePanel(bpy.types.Panel):
layout = self.layout
# Property
- scene.draw_blfield(layout, 'simple_integer')
+ scene.draw_blprop(layout, 'simple_integer')
# Operator
layout.label(text='Look ma, so many hands!')
@@ -63,5 +63,3 @@ class SimplePanel(bpy.types.Panel):
# - Blender Registration
####################
BL_REGISTER = [SimplePanel]
-BL_HANDLERS = None
-BL_KEYMAP_ITEMS = None
diff --git a/examples/simple/blroots_simple_example/preferences.py b/examples/simple/blroots_simple_example/preferences.py
index e58955a..d6a1d9a 100644
--- a/examples/simple/blroots_simple_example/preferences.py
+++ b/examples/simple/blroots_simple_example/preferences.py
@@ -20,7 +20,7 @@ import blroots as blr
import bpy
-class BLExtPrefs(blr.prefs.SensiblePrefs(__package__)):
+class BLExtPrefs(bpy.types.AddonPreferences):
"""Manages user preferences and settings.
Attributes:
@@ -33,6 +33,16 @@ class BLExtPrefs(blr.prefs.SensiblePrefs(__package__)):
bl_idname = __package__
+ # Logging
+ logging_prefs: blr.utils.bl_logger.LoggingPrefs = blr.BLField(
+ blr.gather.blext_info.logging_prefs,
+ display_name='Logging Settings',
+ )
+ ## Methods on this class should include:
+ ## - conform_logger
+ ## - conform_all_loggers_to_prefs
+ ## - draw: Draw these prefs into a layout.
+
####################
# - UI
####################
@@ -46,8 +56,7 @@ class BLExtPrefs(blr.prefs.SensiblePrefs(__package__)):
context: The Blender context object.
"""
layout = self.layout
-
- self.draw_logging_prefs(layout)
+ self.draw_blprop(layout, 'logging_prefs')
####################
diff --git a/examples/simple/uv.lock b/examples/simple/uv.lock
index 130eeff..98932aa 100644
--- a/examples/simple/uv.lock
+++ b/examples/simple/uv.lock
@@ -146,7 +146,32 @@ dev = [
]
[[package]]
-name = "blext-simple-example"
+name = "blroots"
+version = "0.1.0"
+source = { editable = "../../" }
+dependencies = [
+ { name = "griffe" },
+ { name = "pydantic" },
+ { name = "rich" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "griffe", specifier = ">=1.5.5" },
+ { name = "pydantic", specifier = ">=2.10.5" },
+ { name = "rich", specifier = ">=13.9.4" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "commitizen", specifier = ">=4.1.0" },
+ { name = "mypy", specifier = ">=1.14.1" },
+ { name = "pytest", specifier = ">=8.3.4" },
+ { name = "ruff", specifier = ">=0.9.1" },
+]
+
+[[package]]
+name = "blroots-simple-example"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
@@ -178,31 +203,6 @@ dev = [
{ name = "typer", specifier = ">=0.15.1" },
]
-[[package]]
-name = "blroots"
-version = "0.1.0"
-source = { editable = "../../" }
-dependencies = [
- { name = "griffe" },
- { name = "pydantic" },
- { name = "rich" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "griffe", specifier = ">=1.5.5" },
- { name = "pydantic", specifier = ">=2.10.5" },
- { name = "rich", specifier = ">=13.9.4" },
-]
-
-[package.metadata.requires-dev]
-dev = [
- { name = "commitizen", specifier = ">=4.1.0" },
- { name = "mypy", specifier = ">=1.14.1" },
- { name = "pytest", specifier = ">=8.3.4" },
- { name = "ruff", specifier = ">=0.9.1" },
-]
-
[[package]]
name = "click"
version = "8.1.8"
diff --git a/pyproject.toml b/pyproject.toml
index 3167141..c34f84a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -33,6 +33,7 @@ Homepage = "https://codeberg.org/so-rose/blroots"
[dependency-groups]
dev = [
+ "bpy~=4.3.0",
"commitizen>=4.1.0",
"mypy>=1.14.1",
"pytest>=8.3.4",
@@ -155,33 +156,23 @@ quote-style = "single"
indent-style = "tab"
docstring-code-format = false
+
####################
-# - Tooling: MyPy
+# - Tool: basedpyright
####################
-[tool.mypy]
-python_version = '3.11'
-python_executable="./.venv/bin/python"
+[tool.basedpyright]
+defineConstant = { DEBUG = true }
-warn_redundant_casts = true
-warn_unused_ignores = true
-warn_return_any = true
+#include = ["blext"]
+#strict = ["blext"]
-strict_optional = true
-no_implicit_optional = true
+reportMissingImports = "error"
+reportMissingTypeStubs = true
-disallow_subclassing_any = false
-disallow_any_generics = true
-disallow_untyped_calls = true
-disallow_incomplete_defs = true
-
-check_untyped_defs = true
-disallow_untyped_decorators = true
-
-ignore_missing_imports = true
-
-plugins = [
-# 'pydantic.mypy',
-# 'typing_protocol_intersection.mypy_plugin',
+executionEnvironments = [
+ { root = "blroots", pythonVersion = "3.11", extraPaths = [ ".venv/lib/python3.11/site-packages", ".venv/lib/python3.11/site-packages/bpy/4.3/scripts/modules" ] },
+ #{ root = ".", pythonVersion = "3.11", extraPaths = [ ".venv/lib/python3.11/site-packages" ] },
+ { root = "examples/simple", pythonVersion = "3.11", extraPaths = [ ".venv/lib/python3.11/site-packages", ".venv/lib/python3.11/site-packages/bpy/4.3/scripts/modules" ] },
]
diff --git a/uv.lock b/uv.lock
index 0a8aff5..cc0c327 100644
--- a/uv.lock
+++ b/uv.lock
@@ -31,6 +31,7 @@ dependencies = [
[package.dev-dependencies]
dev = [
+ { name = "bpy" },
{ name = "commitizen" },
{ name = "mypy" },
{ name = "pytest" },
@@ -46,12 +47,84 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [
+ { name = "bpy", specifier = ">=4.3.0" },
{ name = "commitizen", specifier = ">=4.1.0" },
{ name = "mypy", specifier = ">=1.14.1" },
{ name = "pytest", specifier = ">=8.3.4" },
{ name = "ruff", specifier = ">=0.9.1" },
]
+[[package]]
+name = "bpy"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cython" },
+ { name = "numpy" },
+ { name = "requests" },
+ { name = "zstandard" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/fc/5902fa508a13f4f1669bc2e2cd8f37adf1caeeed0a344f627c0ffc3bdc40/bpy-4.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82c3486e9762e8cfce4bbc1d6752f5625da8515f6b217e02810350627d693897", size = 217245993 },
+ { url = "https://files.pythonhosted.org/packages/81/39/4e763b150ff05412ab25d5eae91da7f0ade474b5d66509b39a67066d35ec/bpy-4.3.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:490aa94699664a99eee755047485a3ad70470fc6267c55a73459660f52299ade", size = 230465428 },
+ { url = "https://files.pythonhosted.org/packages/9c/4d/897bf3c247d7c6a4e75397f4c5dd5d201fb147981d2271ac35ac955959ab/bpy-4.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1620199392a7e7aab58672aa47c50b3c5b1e4d39d46af5a7f9bbe2af561ea942", size = 377434308 },
+ { url = "https://files.pythonhosted.org/packages/7f/da/c255b626cab58c69c49f4a65d0410b36aa1086dc63862d1c3de653f0a5a0/bpy-4.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a7b2c516aa7a95c31fcaae8842addd07fa86c1ce4768e88a34b480d17e2e93a", size = 333159335 },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.1.31"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
+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 = "1.17.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
+ { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
+ { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
+ { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
+ { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
+ { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
+ { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
+ { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
+ { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
+ { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
+ { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
+ { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
+ { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
+ { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
+ { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
+ { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
+ { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
+ { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
+ { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
+ { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
+ { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
+ { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
+ { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
+ { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
+ { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
+ { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
+ { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
+ { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
+ { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
+ { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
+ { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
+ { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
+ { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
+]
+
[[package]]
name = "charset-normalizer"
version = "3.4.1"
@@ -130,6 +203,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/f7/7f70adfbf3553ffdbe391eaacde72b21dbc1b4226ae56ca32e8ded1bf70b/commitizen-4.1.0-py3-none-any.whl", hash = "sha256:2e6c5fbd442cab4bcc5a04bc86ef2196ef84bcf611317d6c596e87f5bb4c09f5", size = 72282 },
]
+[[package]]
+name = "cython"
+version = "3.0.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/25/886e197c97a4b8e254173002cdc141441e878ff29aaa7d9ba560cd6e4866/cython-3.0.12.tar.gz", hash = "sha256:b988bb297ce76c671e28c97d017b95411010f7c77fa6623dd0bb47eed1aee1bc", size = 2757617 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/60/3d27abd940f7b80a6aeb69dc093a892f04828e1dd0b243dd81ff87d7b0e9/Cython-3.0.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feb86122a823937cc06e4c029d80ff69f082ebb0b959ab52a5af6cdd271c5dc3", size = 3277430 },
+ { url = "https://files.pythonhosted.org/packages/c7/49/f17b0541b317d11f1d021a580643ee2481685157cded92efb32e2fb4daef/Cython-3.0.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfdbea486e702c328338314adb8e80f5f9741f06a0ae83aaec7463bc166d12e8", size = 3444055 },
+ { url = "https://files.pythonhosted.org/packages/6b/7f/c57791ba6a1c934b6f1ab51371e894e3b4bfde0bc35e50046c8754a9d215/Cython-3.0.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563de1728c8e48869d2380a1b76bbc1b1b1d01aba948480d68c1d05e52d20c92", size = 3597874 },
+ { url = "https://files.pythonhosted.org/packages/23/24/803a0db3681b3a2ef65a4bebab201e5ae4aef5e6127ae03683476a573aa9/Cython-3.0.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398d4576c1e1f6316282aa0b4a55139254fbed965cba7813e6d9900d3092b128", size = 3644129 },
+ { url = "https://files.pythonhosted.org/packages/27/13/9b53ba8336e083ece441af8d6d182b8ca83ad523e87c07b3190af379ebc3/Cython-3.0.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1e5eadef80143026944ea8f9904715a008f5108d1d644a89f63094cc37351e73", size = 3504936 },
+ { url = "https://files.pythonhosted.org/packages/a9/d2/d11104be6992a9fe256860cae6d1a79f7dcf3bdb12ae00116fac591f677d/Cython-3.0.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a93cbda00a5451175b97dea5a9440a3fcee9e54b4cba7a7dbcba9a764b22aec", size = 3713066 },
+ { url = "https://files.pythonhosted.org/packages/d9/8c/1fe49135296efa3f460c760a4297f6a5b387f3e69ac5c9dcdbd620295ab3/Cython-3.0.12-cp311-cp311-win32.whl", hash = "sha256:3109e1d44425a2639e9a677b66cd7711721a5b606b65867cb2d8ef7a97e2237b", size = 2579935 },
+ { url = "https://files.pythonhosted.org/packages/02/4e/5ac0b5b9a239cd3fdae187dda8ff06b0b812f671e2501bf253712278f0ac/Cython-3.0.12-cp311-cp311-win_amd64.whl", hash = "sha256:d4b70fc339adba1e2111b074ee6119fe9fd6072c957d8597bce9a0dd1c3c6784", size = 2787337 },
+ { url = "https://files.pythonhosted.org/packages/e6/6c/3be501a6520a93449b1e7e6f63e598ec56f3b5d1bc7ad14167c72a22ddf7/Cython-3.0.12-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fe030d4a00afb2844f5f70896b7f2a1a0d7da09bf3aa3d884cbe5f73fff5d310", size = 3311717 },
+ { url = "https://files.pythonhosted.org/packages/ee/ab/adfeb22c85491de18ae10932165edd5b6f01e4c5e3e363638759d1235015/Cython-3.0.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7fec4f052b8fe173fe70eae75091389955b9a23d5cec3d576d21c5913b49d47", size = 3344337 },
+ { url = "https://files.pythonhosted.org/packages/0d/72/743730d7c46b4c85abefb93187cbbcb7aae8de288d7722b990db3d13499e/Cython-3.0.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0faa5e39e5c8cdf6f9c3b1c3f24972826e45911e7f5b99cf99453fca5432f45e", size = 3517692 },
+ { url = "https://files.pythonhosted.org/packages/09/a1/29a4759a02661f8c8e6b703f62bfbc8285337e6918cc90f55dc0fadb5eb3/Cython-3.0.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d53de996ed340e9ab0fc85a88aaa8932f2591a2746e1ab1c06e262bd4ec4be7", size = 3577057 },
+ { url = "https://files.pythonhosted.org/packages/d6/f8/03d74e98901a7cc2f21f95231b07dd54ec2f69477319bac268b3816fc3a8/Cython-3.0.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea3a0e19ab77266c738aa110684a753a04da4e709472cadeff487133354d6ab8", size = 3396493 },
+ { url = "https://files.pythonhosted.org/packages/50/ea/ac33c5f54f980dbc23dd8f1d5c51afeef26e15ac1a66388e4b8195af83b7/Cython-3.0.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c151082884be468f2f405645858a857298ac7f7592729e5b54788b5c572717ba", size = 3603859 },
+ { url = "https://files.pythonhosted.org/packages/a2/4e/91fc1d6b5e678dcf2d1ecd8dce45b014b4b60d2044d376355c605831c873/Cython-3.0.12-cp312-cp312-win32.whl", hash = "sha256:3083465749911ac3b2ce001b6bf17f404ac9dd35d8b08469d19dc7e717f5877a", size = 2610428 },
+ { url = "https://files.pythonhosted.org/packages/ff/c3/a7fdec227b9f0bb07edbeb016c7b18ed6a8e6ce884d08b2e397cda2c0168/Cython-3.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:c0b91c7ebace030dd558ea28730de8c580680b50768e5af66db2904a3716c3e3", size = 2794755 },
+ { url = "https://files.pythonhosted.org/packages/67/ad/550ddcb8b5a5d9949fe6606595cce36984c1d42309f1e04af98f5933a7ea/Cython-3.0.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4ee6f1ea1bead8e6cbc4e64571505b5d8dbdb3b58e679d31f3a84160cebf1a1a", size = 3393574 },
+ { url = "https://files.pythonhosted.org/packages/34/de/ade0a80bea17197662e23d39d3d3fbf89e9e99e6ad91fd95ab87120edb3a/Cython-3.0.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57aefa6d3341109e46ec1a13e3a763aaa2cbeb14e82af2485b318194be1d9170", size = 3367198 },
+ { url = "https://files.pythonhosted.org/packages/a8/30/7f48207ea13dab46604db0dd388e807d53513ba6ad1c34462892072f8f8c/Cython-3.0.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:879ae9023958d63c0675015369384642d0afb9c9d1f3473df9186c42f7a9d265", size = 3535849 },
+ { url = "https://files.pythonhosted.org/packages/81/ab/f61c79fa14bd433a7dfd1548c5e00d9bd18b557c2f836aaece4fb1b22f34/Cython-3.0.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36fcd584dae547de6f095500a380f4a0cce72b7a7e409e9ff03cb9beed6ac7a1", size = 3559079 },
+ { url = "https://files.pythonhosted.org/packages/d0/d1/1dbf17061229ccd35d5c0eed659fab60c2e50d2eadfa2a5729e753b6f4d0/Cython-3.0.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62b79dcc0de49efe9e84b9d0e2ae0a6fc9b14691a65565da727aa2e2e63c6a28", size = 3436649 },
+ { url = "https://files.pythonhosted.org/packages/2d/d4/9ce42fff6de5550f870cdde9a1482d69ea66a1249a88fa0d0df9adebfb1a/Cython-3.0.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4aa255781b093a8401109d8f2104bbb2e52de7639d5896aefafddc85c30e0894", size = 3644025 },
+ { url = "https://files.pythonhosted.org/packages/e3/89/b0c847f9df92af3ef11281b6811c000bd6f8ce0db02e4374397f8d67f829/Cython-3.0.12-cp313-cp313-win32.whl", hash = "sha256:77d48f2d4bab9fe1236eb753d18f03e8b2619af5b6f05d51df0532a92dfb38ab", size = 2604911 },
+ { url = "https://files.pythonhosted.org/packages/a6/5f/bbfaf2b5f7bf78854ecbc82f8473a3892ae5580e0c5bd0d4a82580b39ed3/Cython-3.0.12-cp313-cp313-win_amd64.whl", hash = "sha256:86c304b20bd57c727c7357e90d5ba1a2b6f1c45492de2373814d7745ef2e63b4", size = 2786786 },
+ { url = "https://files.pythonhosted.org/packages/27/6b/7c87867d255cbce8167ed99fc65635e9395d2af0f0c915428f5b17ec412d/Cython-3.0.12-py2.py3-none-any.whl", hash = "sha256:0038c9bae46c459669390e53a1ec115f8096b2e4647ae007ff1bf4e6dee92806", size = 1171640 },
+]
+
[[package]]
name = "decli"
version = "0.6.2"
@@ -151,6 +257,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/88/52c9422bc853cd7c2b6122090e887d17b5fad29b67f930e4277c9c557357/griffe-1.5.5-py3-none-any.whl", hash = "sha256:2761b1e8876c6f1f9ab1af274df93ea6bbadd65090de5f38f4cb5cc84897c7dd", size = 128221 },
]
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
[[package]]
name = "iniconfig"
version = "2.0.0"
@@ -281,6 +396,54 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
+[[package]]
+name = "numpy"
+version = "2.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/86/453aa3949eab6ff54e2405f9cb0c01f756f031c3dc2a6d60a1d40cba5488/numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8", size = 21237256 },
+ { url = "https://files.pythonhosted.org/packages/20/c3/93ecceadf3e155d6a9e4464dd2392d8d80cf436084c714dc8535121c83e8/numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b", size = 14408049 },
+ { url = "https://files.pythonhosted.org/packages/8d/29/076999b69bd9264b8df5e56f2be18da2de6b2a2d0e10737e5307592e01de/numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a", size = 5408655 },
+ { url = "https://files.pythonhosted.org/packages/e2/a7/b14f0a73eb0fe77cb9bd5b44534c183b23d4229c099e339c522724b02678/numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636", size = 6949996 },
+ { url = "https://files.pythonhosted.org/packages/72/2f/8063da0616bb0f414b66dccead503bd96e33e43685c820e78a61a214c098/numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d", size = 14355789 },
+ { url = "https://files.pythonhosted.org/packages/e6/d7/3cd47b00b8ea95ab358c376cf5602ad21871410950bc754cf3284771f8b6/numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb", size = 16411356 },
+ { url = "https://files.pythonhosted.org/packages/27/c0/a2379e202acbb70b85b41483a422c1e697ff7eee74db642ca478de4ba89f/numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2", size = 15576770 },
+ { url = "https://files.pythonhosted.org/packages/bc/63/a13ee650f27b7999e5b9e1964ae942af50bb25606d088df4229283eda779/numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b", size = 18200483 },
+ { url = "https://files.pythonhosted.org/packages/4c/87/e71f89935e09e8161ac9c590c82f66d2321eb163893a94af749dfa8a3cf8/numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5", size = 6588415 },
+ { url = "https://files.pythonhosted.org/packages/b9/c6/cd4298729826af9979c5f9ab02fcaa344b82621e7c49322cd2d210483d3f/numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f", size = 12929604 },
+ { url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 },
+ { url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 },
+ { url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 },
+ { url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 },
+ { url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 },
+ { url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 },
+ { url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 },
+ { url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 },
+ { url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 },
+ { url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 },
+ { url = "https://files.pythonhosted.org/packages/0e/8b/88b98ed534d6a03ba8cddb316950fe80842885709b58501233c29dfa24a9/numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba", size = 20916001 },
+ { url = "https://files.pythonhosted.org/packages/d9/b4/def6ec32c725cc5fbd8bdf8af80f616acf075fe752d8a23e895da8c67b70/numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50", size = 14130721 },
+ { url = "https://files.pythonhosted.org/packages/20/60/70af0acc86495b25b672d403e12cb25448d79a2b9658f4fc45e845c397a8/numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1", size = 5130999 },
+ { url = "https://files.pythonhosted.org/packages/2e/69/d96c006fb73c9a47bcb3611417cf178049aae159afae47c48bd66df9c536/numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5", size = 6665299 },
+ { url = "https://files.pythonhosted.org/packages/5a/3f/d8a877b6e48103733ac224ffa26b30887dc9944ff95dffdfa6c4ce3d7df3/numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2", size = 14064096 },
+ { url = "https://files.pythonhosted.org/packages/e4/43/619c2c7a0665aafc80efca465ddb1f260287266bdbdce517396f2f145d49/numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1", size = 16114758 },
+ { url = "https://files.pythonhosted.org/packages/d9/79/ee4fe4f60967ccd3897aa71ae14cdee9e3c097e3256975cc9575d393cb42/numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304", size = 15259880 },
+ { url = "https://files.pythonhosted.org/packages/fb/c8/8b55cf05db6d85b7a7d414b3d1bd5a740706df00bfa0824a08bf041e52ee/numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d", size = 17876721 },
+ { url = "https://files.pythonhosted.org/packages/21/d6/b4c2f0564b7dcc413117b0ffbb818d837e4b29996b9234e38b2025ed24e7/numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693", size = 6290195 },
+ { url = "https://files.pythonhosted.org/packages/97/e7/7d55a86719d0de7a6a597949f3febefb1009435b79ba510ff32f05a8c1d7/numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b", size = 12619013 },
+ { url = "https://files.pythonhosted.org/packages/a6/1f/0b863d5528b9048fd486a56e0b97c18bf705e88736c8cea7239012119a54/numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890", size = 20944621 },
+ { url = "https://files.pythonhosted.org/packages/aa/99/b478c384f7a0a2e0736177aafc97dc9152fc036a3fdb13f5a3ab225f1494/numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c", size = 14142502 },
+ { url = "https://files.pythonhosted.org/packages/fb/61/2d9a694a0f9cd0a839501d362de2a18de75e3004576a3008e56bdd60fcdb/numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94", size = 5176293 },
+ { url = "https://files.pythonhosted.org/packages/33/35/51e94011b23e753fa33f891f601e5c1c9a3d515448659b06df9d40c0aa6e/numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0", size = 6691874 },
+ { url = "https://files.pythonhosted.org/packages/ff/cf/06e37619aad98a9d03bd8d65b8e3041c3a639be0f5f6b0a0e2da544538d4/numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610", size = 14036826 },
+ { url = "https://files.pythonhosted.org/packages/0c/93/5d7d19955abd4d6099ef4a8ee006f9ce258166c38af259f9e5558a172e3e/numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76", size = 16096567 },
+ { url = "https://files.pythonhosted.org/packages/af/53/d1c599acf7732d81f46a93621dab6aa8daad914b502a7a115b3f17288ab2/numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a", size = 15242514 },
+ { url = "https://files.pythonhosted.org/packages/53/43/c0f5411c7b3ea90adf341d05ace762dad8cb9819ef26093e27b15dd121ac/numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf", size = 17872920 },
+ { url = "https://files.pythonhosted.org/packages/5b/57/6dbdd45ab277aff62021cafa1e15f9644a52f5b5fc840bc7591b4079fb58/numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef", size = 6346584 },
+ { url = "https://files.pythonhosted.org/packages/97/9b/484f7d04b537d0a1202a5ba81c6f53f1846ae6c63c2127f8df869ed31342/numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082", size = 12706784 },
+]
+
[[package]]
name = "packaging"
version = "24.2"
@@ -311,6 +474,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 },
]
+[[package]]
+name = "pycparser"
+version = "2.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
+]
+
[[package]]
name = "pydantic"
version = "2.10.5"
@@ -449,6 +621,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747 },
]
+[[package]]
+name = "requests"
+version = "2.32.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
+]
+
[[package]]
name = "rich"
version = "13.9.4"
@@ -514,6 +701,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
+[[package]]
+name = "urllib3"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
+]
+
[[package]]
name = "wcwidth"
version = "0.2.13"
@@ -522,3 +718,62 @@ sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
]
+
+[[package]]
+name = "zstandard"
+version = "0.23.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation == 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699 },
+ { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681 },
+ { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328 },
+ { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955 },
+ { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944 },
+ { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927 },
+ { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910 },
+ { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544 },
+ { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094 },
+ { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440 },
+ { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091 },
+ { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682 },
+ { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707 },
+ { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792 },
+ { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586 },
+ { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420 },
+ { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713 },
+ { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459 },
+ { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707 },
+ { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545 },
+ { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533 },
+ { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510 },
+ { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973 },
+ { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968 },
+ { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179 },
+ { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577 },
+ { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899 },
+ { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964 },
+ { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398 },
+ { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313 },
+ { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877 },
+ { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595 },
+ { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 },
+ { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 },
+ { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 },
+ { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 },
+ { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 },
+ { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 },
+ { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 },
+ { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 },
+ { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 },
+ { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 },
+ { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 },
+ { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 },
+ { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 },
+ { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 },
+ { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 },
+ { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 },
+]