feat: blob of fixes and features

main
Sofus Albert Høgsbro Rose 2025-02-19 11:54:47 +01:00
parent 431aec6400
commit dc9a080996
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
26 changed files with 1129 additions and 532 deletions

View File

@ -5,17 +5,17 @@ from .gather import BLExtInfo, blext_info
from .utils import bl_logger as log from .utils import bl_logger as log
__all__ = [ __all__ = [
'BLExtInfo',
'BLField',
'bl_prop', 'bl_prop',
'blext_dir',
'blext_info',
'blext_prefs',
'gather', 'gather',
'init', 'init',
'log',
'log',
'prefs', 'prefs',
'registration', 'registration',
'typ', 'typ',
'blext_dir',
'blext_prefs',
'BLField',
'BLExtInfo',
'log',
'blext_info',
'bl_logger',
] ]

View File

@ -5,10 +5,10 @@ from .bl_prop import BLProp, SupportedBLPropType
from .signal import Signal from .signal import Signal
__all__ = [ __all__ = [
'types',
'BLField', 'BLField',
'BLIDProps', 'BLIDProps',
'BLProp', 'BLProp',
'SupportedBLPropType',
'Signal', 'Signal',
'SupportedBLPropType',
'types',
] ]

View File

@ -23,12 +23,9 @@ In particular,
import functools import functools
import typing as typ import typing as typ
import bpy
# import griffe # import griffe
from .bl_id_props import BLIDProps from .bl_id_props import BLIDProps
from .bl_prop import BLProp, SupportedBLPropType from .bl_prop import BLProp, SupportedBLPropType
from .signal import Signal
#################### ####################
@ -44,7 +41,7 @@ class BLField:
display_name: str | None = None, display_name: str | None = None,
description: str | None = None, description: str | None = None,
**bl_prop_info: dict[str, typ.Any], **bl_prop_info: dict[str, typ.Any],
): ) -> None:
"""Initializes and sets the attribute to a given default value. """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`. The attribute **must** declare a type annotation, and it **must** match the type of `default_value`.
@ -63,35 +60,6 @@ class BLField:
#################### ####################
# - Initialization # - 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: def __set_name__(self, owner: type[SupportedBLPropType], name: str) -> None:
"""Sets up this descriptor on the class, preparing it for per-instance use. """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. 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. 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( if issubclass(
owner, owner,
BLIDProps | SupportedBLPropType, BLIDProps | SupportedBLPropType,
@ -139,12 +109,13 @@ class BLField:
msg = f"Cannot declare 'BLField' (name='{name}') on unsupported class '{owner}'." msg = f"Cannot declare 'BLField' (name='{name}') on unsupported class '{owner}'."
raise TypeError(msg) raise TypeError(msg)
# Initialize BLProp on Owner ####################
## - Don't initialize a BLField on BLIDProps, since it modifies a global bpy type. # - Initialize Owner
## - Instead, its register() method must do the same sequence. ####################
## This is only done if the owner should directly accessed at this point.
## Ex. BLIDProps has its own register() method.
if issubclass(owner, SupportedBLPropType): if issubclass(owner, SupportedBLPropType):
self.bl_prop.init_on_bl_class(owner) self.bl_prop.init_on_bl_class(owner)
self.init_on_bl_class(owner)
@functools.cached_property @functools.cached_property
def bl_prop(self) -> BLProp: def bl_prop(self) -> BLProp:
@ -194,27 +165,7 @@ class BLField:
bl_instance: Instance that is accessing the attribute. bl_instance: Instance that is accessing the attribute.
owner: The class that owns the instance. owner: The class that owns the instance.
""" """
# Read the Non-Persistent Cached Value return self.bl_prop.read(bl_instance)
## - 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
def __set__(self, bl_instance: SupportedBLPropType | None, value: typ.Any) -> None: def __set__(self, bl_instance: SupportedBLPropType | None, value: typ.Any) -> None:
"""Sets the value described by the BLField. """Sets the value described by the BLField.
@ -230,23 +181,4 @@ class BLField:
bl_instance: Instance that is accessing the attribute. bl_instance: Instance that is accessing the attribute.
owner: The class that owns the instance. owner: The class that owns the instance.
""" """
match value: self.bl_prop.write(bl_instance, 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)

View File

@ -32,9 +32,11 @@ SupportedBLPropType: typ.TypeAlias = (
) )
#################### def bl_prop_instance_property_name(bl_prop_name: str) -> str:
# - Blender Property (Abstraction) """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): class BLProp(pyd.BaseModel, frozen=True):
"""Convenient, fast, and safe interface to a property on a Blender class. """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 default_value: typ.Any
# TODO: Support certain property flags universally. use_property_group: bool = False
# TODO: Support certain property flags universally?
#################### ####################
# - Computed # - 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.""" """Deduces the internal attribute name where the raw Blender property data will be persisted."""
return f'blprop__{self.name}' return f'blprop__{self.name}'
#################### @functools.cached_property
# - Type Support def instance_bl_name(self):
#################### """Deduces the internal attribute name where a reference to this class instance will be persisted."""
@classmethod return bl_prop_instance_property_name(self.name)
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
#################### ####################
# - Creation # - Creation
@ -207,6 +151,7 @@ class BLProp(pyd.BaseModel, frozen=True):
self, self,
bl_class: type[SupportedBLPropType], bl_class: type[SupportedBLPropType],
) -> None: ) -> 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. """Declare the Blender property on a Blender class, ensuring that the property will be available to all `SupportedBLPropType` respecting instances of that class.
Parameters: Parameters:
@ -214,22 +159,34 @@ class BLProp(pyd.BaseModel, frozen=True):
**Must** be chosen such that `BLPropType.from_type(obj_type) == self`. **Must** be chosen such that `BLPropType.from_type(obj_type) == self`.
""" """
# Add Method: Reset Instance ID ####################
def reset_instance_id(bl_instance: SupportedBLPropType) -> None: # - Add Method: reset_instance_id
bl_instance.instance_id = str(uuid.uuid4()) ####################
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): if issubclass(bl_class, bpy.types.ID):
# Set Instance ID # Set Instance ID
if not hasattr(bl_class, 'instance_id'): if not hasattr(bl_class, 'instance_id'):
bl_class.instance_id = bpy.props.StringProperty() bl_class.instance_id = bpy.props.StringProperty()
# Set Property # 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( elif issubclass(
bl_class, bl_class,
bpy.types.AddonPreferences bpy.types.AddonPreferences
@ -244,8 +201,116 @@ class BLProp(pyd.BaseModel, frozen=True):
# Set Annotation # Set Annotation
bl_class.__annotations__[self.bl_name] = self.bpy_prop 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: def read_nonpersist(self, bl_instance: SupportedBLPropType | None) -> typ.Any:
"""Read the non-persistent cache value for this property. """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 bl_instance: The Blender object to
**NOTE**: `bl_instance` must not be `None`, as neighboring methods sometimes allow. **NOTE**: `bl_instance` must not be `None`, as neighboring methods sometimes allow.
""" """
signal = bl_prop_cache.write( if self.use_property_group:
bl_instance, for subkey, subvalue in value.items():
key=self.bl_name, signal = bl_prop_cache.write(
value=self.encode(value), getattr(bl_instance, self.bl_name),
use_nonpersist=False, key=subkey,
use_persist=True, value=subvalue,
) use_nonpersist=False,
use_persist=True,
)
if signal is Signal.CacheNotReady: 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." 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) 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) self.write_nonpersist(bl_instance, value)
def invalidate_nonpersist(self, bl_instance: SupportedBLPropType | None) -> None: def invalidate_nonpersist(self, bl_instance: SupportedBLPropType | None) -> None:
@ -361,3 +442,73 @@ class BLProp(pyd.BaseModel, frozen=True):
bl_instance, bl_instance,
self.bl_name, 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

View File

@ -1,11 +1,15 @@
from .bool_prop import BoolProp from .bool_prop import BoolProp
from .float_prop import FloatProp from .float_prop import FloatProp
from .int_prop import IntProp from .int_prop import IntProp
from .path_prop import PathProp
from .str_enum_prop import StrEnumProp
from .str_prop import StrProp from .str_prop import StrProp
__all__ = [ __all__ = [
'BoolProp', 'BoolProp',
'FloatProp', 'FloatProp',
'IntProp', 'IntProp',
'PathProp',
'StrEnumProp',
'StrProp', 'StrProp',
] ]

View File

@ -91,6 +91,7 @@ class BoolProp(BLProp, frozen=True):
self, self,
bl_instance: SupportedBLPropType, bl_instance: SupportedBLPropType,
layout: bpy.types.UILayout, layout: bpy.types.UILayout,
text: str | None = None,
toggle: bool = False, toggle: bool = False,
expand: bool = False, expand: bool = False,
) -> None: ) -> None:
@ -98,7 +99,7 @@ class BoolProp(BLProp, frozen=True):
layout.prop( layout.prop(
bl_instance, bl_instance,
self.bl_name, self.bl_name,
text=self.display_name, text=self.display_name if text is None else text,
toggle=toggle, toggle=toggle,
expand=expand, expand=expand,
) )

View File

@ -24,25 +24,62 @@ import bpy
from blroots.utils import bl_instance, logger 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__) log = logger.get(__name__)
PRIORITY = 11 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): class ClassProp(BLProp, frozen=True):
"""A single constrained floating-point number. """A single class; in particular, an appropriately configured `pydantic` BaseModel."""
Parameters: use_property_group = True
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: None default_value: None
@ -51,17 +88,23 @@ class ClassProp(BLProp, frozen=True):
#################### ####################
@classmethod @classmethod
def supports_type_hint(cls, field_type_hint: type) -> bool: def supports_type_hint(cls, field_type_hint: type) -> bool:
"""Support only the explicit type `float` aka. `builtins.float`.""" """Support classes that conform to the `BLPropClass` protocol."""
return inspect.isclass(field_type_hint) and hasattr( return inspect.isclass(field_type_hint) and isinstance(
field_type_hint, 'property_group' field_type_hint, BLPropClass
) )
@property @property
def bpy_property_group(self) -> type[bpy.props.PropertyGroup]: def bpy_property_group(self) -> type[bpy.props.PropertyGroup]:
"""Dynamically create a PropertyGroup.""" """Dynamically create a PropertyGroup."""
## TODO: We can make it; the trouble is, who registers it, and when? return self.type_hint.bl_prop_group()
## - Also, there's an underlying class instance ## TODO: Who registers it, and when?
pass ## 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 # - bpy.props Interface
@ -69,7 +112,7 @@ class ClassProp(BLProp, frozen=True):
@property @property
def bpy_prop_type(self) -> type[bpy.props.PointerProperty]: def bpy_prop_type(self) -> type[bpy.props.PointerProperty]:
"""Use `bpy.props.PointerProperty` to reference a particular pre-made `bpy.types.PropertyGroup` subclass.""" """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 @functools.cached_property
def bpy_prop_kwargs(self) -> dict[str, typ.Any]: def bpy_prop_kwargs(self) -> dict[str, typ.Any]:
@ -81,17 +124,48 @@ class ClassProp(BLProp, frozen=True):
#################### ####################
# - Encode/Decode # - Encode/Decode
#################### ####################
def encode(self, value: typ.Any) -> float: def encode(self, value: typ.Any) -> dict[str, typ.Any]:
"""Encode a value that supports `float()` to a Blender float.""" """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): 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? return {
pass 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 raise NotImplementedError
def decode(self, raw_bl_value: float) -> float: def decode(self, raw_bl_value: bpy.types.PropertyGroup) -> typ.Any:
"""Decode a Blender float by doing nothing.""" """Decode the subclass of `bpy.types.PropertyGroup` subclass into the `pydantic.BaseModel` contained within `self.type_hint`.
return raw_bl_value
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 # - UI

View File

@ -122,10 +122,16 @@ class FloatProp(BLProp, frozen=True):
self, self,
bl_instance: SupportedBLPropType, bl_instance: SupportedBLPropType,
layout: bpy.types.UILayout, layout: bpy.types.UILayout,
text: str | None = None,
slider: bool = False, slider: bool = False,
) -> None: ) -> None:
"""Draw this property to the given Blender `UILayout`.""" """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 # - Selection

View File

@ -111,12 +111,16 @@ class IntProp(BLProp, frozen=True):
self, self,
bl_instance: SupportedBLPropType, bl_instance: SupportedBLPropType,
layout: bpy.types.UILayout, layout: bpy.types.UILayout,
text: str | None = None,
slider: bool = False, slider: bool = False,
) -> None: ) -> None:
"""Draw this property to the given Blender `UILayout`.""" """Draw this property to the given Blender `UILayout`."""
## TODO: layout.prop(
## - Support for slider bl_instance,
layout.prop(bl_instance, self.bl_name, text=self.display_name, slider=slider) self.bl_name,
text=self.display_name if text is None else text,
slider=slider,
)
#################### ####################
# - Selection # - Selection

View File

@ -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 <http://www.gnu.org/licenses/>.
"""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

View File

@ -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 <http://www.gnu.org/licenses/>.
"""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

View File

@ -28,16 +28,11 @@ PRIORITY = 20
class StrProp(BLProp, frozen=True): class StrProp(BLProp, frozen=True):
"""A single constrained floating-point number. """A single constrained string.
Parameters: Parameters:
abs_min: The absolute minimum value of the floating point number. is_secret: Whether the string should be considered a secret value.
abs_max: The absolute maximum value of the floating point number. Secrets display as dots in the interface, and aren't saved in the `.blend` on save.
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: str default_value: str
@ -101,11 +96,15 @@ class StrProp(BLProp, frozen=True):
self, self,
bl_instance: SupportedBLPropType, bl_instance: SupportedBLPropType,
layout: bpy.types.UILayout, layout: bpy.types.UILayout,
text: str | None = None,
placeholder: str = '', placeholder: str = '',
) -> None: ) -> None:
"""Draw this property to the given Blender `UILayout`.""" """Draw this property to the given Blender `UILayout`."""
layout.prop( 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,
) )
#################### ####################

View File

@ -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

View File

@ -1,4 +1,4 @@
# blext # blroots
# Copyright (C) 2025 blext Project Contributors # Copyright (C) 2025 blext Project Contributors
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify

View File

@ -23,11 +23,12 @@ import typing as typ
from pathlib import Path from pathlib import Path
import bpy import bpy
import pydantic as pyd
import rich.console import rich.console
import rich.logging import rich.logging
import rich.traceback import rich.traceback
from blroots import gather from blroots import bl_prop, gather
#################### ####################
# - Configuration # - Configuration
@ -53,12 +54,12 @@ LogLevel: typ.TypeAlias = int
class BLExtLogLevel(enum.StrEnum): class BLExtLogLevel(enum.StrEnum):
"""Log level of a Blender extension. """Log level of a Blender extension.
Attributes: Attributes:
Debug: Low-level messages for debugging. Debug: Low-level messages for debugging.
Info: Communications about routine operations. Info: Communications about routine operations.
Warning: Something that might be wrong. Warning: Something that might be wrong.
Error: Something wrong that can be recovered from. Error: Something wrong that can be recovered from.
Critical: Something wrong that cannot be recovered from. Critical: Something wrong that cannot be recovered from.
""" """
Debug = enum.auto() Debug = enum.auto()
@ -292,3 +293,165 @@ def sensible_file_handler(
file_handler.setFormatter(file_formatter) file_handler.setFormatter(file_formatter)
file_handler.setLevel(level) file_handler.setLevel(level)
return file_handler 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='')

View File

@ -1,5 +1,5 @@
# blext # blroots
# Copyright (C) 2025 blext Project Contributors # Copyright (C) 2025 blroots Project Contributors
# #
# This program is free software: you can redistribute it and/or modify # 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 # 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 # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Provides identifiers for Blender operators defined by this addon."""
import enum import enum
import blroots as blr import blroots as blr
@ -23,7 +20,17 @@ import blroots as blr
BLEXT_INFO = blr.blext_info(__name__) BLEXT_INFO = blr.blext_info(__name__)
class Icon(enum.StrEnum):
"""Identifiers for icons used throughout this addon."""
class OperatorType(enum.StrEnum): class OperatorType(enum.StrEnum):
"""Identifiers for addon-defined `bpy.types.Operator`.""" """Identifiers for addon-defined `bpy.types.Operator`."""
SimpleOperator = BLEXT_INFO.operator_name('simple_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')

View File

@ -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 <http://www.gnu.org/licenses/>.
"""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',
]

View File

@ -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 <http://www.gnu.org/licenses/>.
"""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."""

View File

@ -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 <http://www.gnu.org/licenses/>.
"""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')

View File

@ -40,10 +40,9 @@ class SimpleOperator(bpy.types.Operator):
def execute(self, context: bpy.types.Context) -> blr.typ.BLOperatorStatus: def execute(self, context: bpy.types.Context) -> blr.typ.BLOperatorStatus:
"""Display a simple message on execution.""" """Display a simple message on execution."""
scene = context.scene
self.report( self.report(
{'INFO'}, {'INFO'},
f'The simple integer was set to {scene.simple_integer}.', f'The simple integer was set to {context.scene.simple_integer}.',
) )
return {'FINISHED'} return {'FINISHED'}
@ -53,5 +52,3 @@ class SimpleOperator(bpy.types.Operator):
# - Blender Registration # - Blender Registration
#################### ####################
BL_REGISTER = [SimpleOperator] BL_REGISTER = [SimpleOperator]
BL_HANDLERS = None
BL_KEYMAP_ITEMS = None

View File

@ -52,7 +52,7 @@ class SimplePanel(bpy.types.Panel):
layout = self.layout layout = self.layout
# Property # Property
scene.draw_blfield(layout, 'simple_integer') scene.draw_blprop(layout, 'simple_integer')
# Operator # Operator
layout.label(text='Look ma, so many hands!') layout.label(text='Look ma, so many hands!')
@ -63,5 +63,3 @@ class SimplePanel(bpy.types.Panel):
# - Blender Registration # - Blender Registration
#################### ####################
BL_REGISTER = [SimplePanel] BL_REGISTER = [SimplePanel]
BL_HANDLERS = None
BL_KEYMAP_ITEMS = None

View File

@ -20,7 +20,7 @@ import blroots as blr
import bpy import bpy
class BLExtPrefs(blr.prefs.SensiblePrefs(__package__)): class BLExtPrefs(bpy.types.AddonPreferences):
"""Manages user preferences and settings. """Manages user preferences and settings.
Attributes: Attributes:
@ -33,6 +33,16 @@ class BLExtPrefs(blr.prefs.SensiblePrefs(__package__)):
bl_idname = __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 # - UI
#################### ####################
@ -46,8 +56,7 @@ class BLExtPrefs(blr.prefs.SensiblePrefs(__package__)):
context: The Blender context object. context: The Blender context object.
""" """
layout = self.layout layout = self.layout
self.draw_blprop(layout, 'logging_prefs')
self.draw_logging_prefs(layout)
#################### ####################

View File

@ -146,7 +146,32 @@ dev = [
] ]
[[package]] [[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" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
@ -178,31 +203,6 @@ dev = [
{ name = "typer", specifier = ">=0.15.1" }, { 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]] [[package]]
name = "click" name = "click"
version = "8.1.8" version = "8.1.8"

View File

@ -33,6 +33,7 @@ Homepage = "https://codeberg.org/so-rose/blroots"
[dependency-groups] [dependency-groups]
dev = [ dev = [
"bpy~=4.3.0",
"commitizen>=4.1.0", "commitizen>=4.1.0",
"mypy>=1.14.1", "mypy>=1.14.1",
"pytest>=8.3.4", "pytest>=8.3.4",
@ -155,33 +156,23 @@ quote-style = "single"
indent-style = "tab" indent-style = "tab"
docstring-code-format = false docstring-code-format = false
#################### ####################
# - Tooling: MyPy # - Tool: basedpyright
#################### ####################
[tool.mypy] [tool.basedpyright]
python_version = '3.11' defineConstant = { DEBUG = true }
python_executable="./.venv/bin/python"
warn_redundant_casts = true #include = ["blext"]
warn_unused_ignores = true #strict = ["blext"]
warn_return_any = true
strict_optional = true reportMissingImports = "error"
no_implicit_optional = true reportMissingTypeStubs = true
disallow_subclassing_any = false executionEnvironments = [
disallow_any_generics = true { root = "blroots", pythonVersion = "3.11", extraPaths = [ ".venv/lib/python3.11/site-packages", ".venv/lib/python3.11/site-packages/bpy/4.3/scripts/modules" ] },
disallow_untyped_calls = true #{ root = ".", pythonVersion = "3.11", extraPaths = [ ".venv/lib/python3.11/site-packages" ] },
disallow_incomplete_defs = true { root = "examples/simple", pythonVersion = "3.11", extraPaths = [ ".venv/lib/python3.11/site-packages", ".venv/lib/python3.11/site-packages/bpy/4.3/scripts/modules" ] },
check_untyped_defs = true
disallow_untyped_decorators = true
ignore_missing_imports = true
plugins = [
# 'pydantic.mypy',
# 'typing_protocol_intersection.mypy_plugin',
] ]

255
uv.lock
View File

@ -31,6 +31,7 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "bpy" },
{ name = "commitizen" }, { name = "commitizen" },
{ name = "mypy" }, { name = "mypy" },
{ name = "pytest" }, { name = "pytest" },
@ -46,12 +47,84 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "bpy", specifier = ">=4.3.0" },
{ name = "commitizen", specifier = ">=4.1.0" }, { name = "commitizen", specifier = ">=4.1.0" },
{ name = "mypy", specifier = ">=1.14.1" }, { name = "mypy", specifier = ">=1.14.1" },
{ name = "pytest", specifier = ">=8.3.4" }, { name = "pytest", specifier = ">=8.3.4" },
{ name = "ruff", specifier = ">=0.9.1" }, { 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]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.1" 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 }, { 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]] [[package]]
name = "decli" name = "decli"
version = "0.6.2" 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 }, { 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]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.0.0" 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 }, { 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]] [[package]]
name = "packaging" name = "packaging"
version = "24.2" 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 }, { 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]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.10.5" 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 }, { 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]] [[package]]
name = "rich" name = "rich"
version = "13.9.4" 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 }, { 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]] [[package]]
name = "wcwidth" name = "wcwidth"
version = "0.2.13" version = "0.2.13"
@ -522,3 +718,62 @@ sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, { 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 },
]