diff --git a/pyproject.toml b/pyproject.toml index 7989ea3..4780d8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "haiway" description = "Framework for dependency injection and state management within structured concurrency model." -version = "0.8.4" +version = "0.9.0" readme = "README.md" maintainers = [ { name = "Kacper Kaliński", email = "kacper.kalinski@miquido.com" }, diff --git a/src/haiway/__init__.py b/src/haiway/__init__.py index 66df433..14dce46 100644 --- a/src/haiway/__init__.py +++ b/src/haiway/__init__.py @@ -3,6 +3,7 @@ Disposables, MetricsContext, MetricsHandler, + MetricsReading, MetricsRecording, MetricsScopeEntering, MetricsScopeExiting, @@ -13,6 +14,7 @@ ) from haiway.helpers import ( ArgumentsTrace, + MetricsHolder, MetricsLogger, ResultTrace, asynchronous, @@ -61,7 +63,9 @@ "Disposables", "MetricsContext", "MetricsHandler", + "MetricsHolder", "MetricsLogger", + "MetricsReading", "MetricsRecording", "MetricsScopeEntering", "MetricsScopeExiting", diff --git a/src/haiway/context/__init__.py b/src/haiway/context/__init__.py index 05657e2..3e3fa88 100644 --- a/src/haiway/context/__init__.py +++ b/src/haiway/context/__init__.py @@ -4,6 +4,7 @@ from haiway.context.metrics import ( MetricsContext, MetricsHandler, + MetricsReading, MetricsRecording, MetricsScopeEntering, MetricsScopeExiting, @@ -15,6 +16,7 @@ "Disposables", "MetricsContext", "MetricsHandler", + "MetricsReading", "MetricsRecording", "MetricsScopeEntering", "MetricsScopeExiting", diff --git a/src/haiway/context/access.py b/src/haiway/context/access.py index f477029..63b525f 100644 --- a/src/haiway/context/access.py +++ b/src/haiway/context/access.py @@ -423,6 +423,62 @@ def record( MetricsContext.record(metric) + @overload + @staticmethod + async def read[Metric: State]( + metric: type[Metric], + /, + *, + merged: bool = False, + ) -> Metric | None: ... + + @overload + @staticmethod + async def read[Metric: State]( + metric: type[Metric], + /, + *, + merged: bool = False, + default: Metric, + ) -> Metric: ... + + @staticmethod + async def read[Metric: State]( + metric: type[Metric], + /, + *, + merged: bool = False, + default: Metric | None = None, + ) -> Metric | None: + """ + Read metric within current scope context. + + Parameters + ---------- + metric: type[Metric] + type of metric to be read from current context. + + merged: bool + control wheather to merge metrics from nested scopes (True)\ + or access only the current scope value (False) without combining them + + default: Metric | None + default value to return when metric was not recorded yet. + + Returns + ------- + Metric | None + """ + + value: Metric | None = await MetricsContext.read( + metric, + merged=merged, + ) + if value is None: + return default + + return value + @staticmethod def log_error( message: str, diff --git a/src/haiway/context/metrics.py b/src/haiway/context/metrics.py index 85a4849..66271c9 100644 --- a/src/haiway/context/metrics.py +++ b/src/haiway/context/metrics.py @@ -9,6 +9,7 @@ __all__ = [ "MetricsContext", "MetricsHandler", + "MetricsReading", "MetricsRecording", "MetricsScopeEntering", "MetricsScopeExiting", @@ -25,6 +26,18 @@ def __call__( ) -> None: ... +@runtime_checkable +class MetricsReading(Protocol): + async def __call__[Metric: State]( + self, + scope: ScopeIdentifier, + /, + *, + metric: type[Metric], + merged: bool, + ) -> Metric | None: ... + + @runtime_checkable class MetricsScopeEntering(Protocol): def __call__[Metric: State]( @@ -45,6 +58,7 @@ def __call__[Metric: State]( class MetricsHandler(State): record: MetricsRecording + read: MetricsReading enter_scope: MetricsScopeEntering exit_scope: MetricsScopeExiting @@ -100,6 +114,30 @@ def record( exception=exc, ) + @classmethod + async def read[Metric: State]( + cls, + metric: type[Metric], + /, + merged: bool, + ) -> Metric | None: + try: # catch exceptions - we don't wan't to blow up on metrics + metrics: Self = cls._context.get() + + if metrics._metrics is not None: + return await metrics._metrics.read( + metrics._scope, + metric=metric, + merged=merged, + ) + + except Exception as exc: + LoggerContext.log_error( + "Failed to read metric: %s", + metric.__qualname__, + exception=exc, + ) + def __init__( self, scope: ScopeIdentifier, diff --git a/src/haiway/helpers/__init__.py b/src/haiway/helpers/__init__.py index bf0a79b..c5e2b48 100644 --- a/src/haiway/helpers/__init__.py +++ b/src/haiway/helpers/__init__.py @@ -1,6 +1,6 @@ from haiway.helpers.asynchrony import asynchronous, wrap_async from haiway.helpers.caching import cache -from haiway.helpers.metrics import MetricsLogger +from haiway.helpers.metrics import MetricsHolder, MetricsLogger from haiway.helpers.retries import retry from haiway.helpers.throttling import throttle from haiway.helpers.timeouted import timeout @@ -8,6 +8,7 @@ __all__ = [ "ArgumentsTrace", + "MetricsHolder", "MetricsLogger", "ResultTrace", "asynchronous", diff --git a/src/haiway/helpers/asynchrony.py b/src/haiway/helpers/asynchrony.py index 97b4020..8a16fa4 100644 --- a/src/haiway/helpers/asynchrony.py +++ b/src/haiway/helpers/asynchrony.py @@ -30,12 +30,10 @@ async def async_function(*args: Args.args, **kwargs: Args.kwargs) -> Result: @overload -def asynchronous[**Args, Result]() -> ( - Callable[ - [Callable[Args, Result]], - Callable[Args, Coroutine[None, None, Result]], - ] -): ... +def asynchronous[**Args, Result]() -> Callable[ + [Callable[Args, Result]], + Callable[Args, Coroutine[None, None, Result]], +]: ... @overload diff --git a/src/haiway/helpers/metrics.py b/src/haiway/helpers/metrics.py index ad4583f..1f18522 100644 --- a/src/haiway/helpers/metrics.py +++ b/src/haiway/helpers/metrics.py @@ -1,14 +1,15 @@ from collections.abc import Sequence from itertools import chain from time import monotonic -from typing import Any, Self, cast, final +from typing import Any, Self, cast, final, overload from haiway.context import MetricsHandler, ScopeIdentifier, ctx from haiway.state import State -from haiway.types import MISSING, Missing +from haiway.types import MISSING __all_ = [ "MetricsLogger", + "MetricsHolder", ] @@ -32,25 +33,129 @@ def time(self) -> float: def finished(self) -> float: return self.exited is not None and all(nested.finished for nested in self.nested) - def merged(self) -> Sequence[State]: - merged_metrics: dict[type[State], State] = dict(self.metrics) - for element in chain.from_iterable(nested.merged() for nested in self.nested): - metric_type: type[State] = type(element) - current: State | Missing = merged_metrics.get( - metric_type, - MISSING, - ) + @overload + def merged[Metric: State]( + self, + ) -> Sequence[State]: ... - if current is MISSING: - continue # do not merge to missing + @overload + def merged[Metric: State]( + self, + metric: type[Metric], + ) -> Metric | None: ... - elif hasattr(current, "__add__"): - merged_metrics[metric_type] = current.__add__(element) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue] + def merged[Metric: State]( + self, + metric: type[Metric] | None = None, + ) -> Sequence[State] | Metric | None: + if metric is None: + merged_metrics: dict[type[State], State] = dict(self.metrics) + for nested in chain.from_iterable(nested.merged() for nested in self.nested): + metric_type: type[State] = type(nested) + current: State | None = merged_metrics.get(metric_type) - else: - merged_metrics[metric_type] = element + if current is None: + merged_metrics[metric_type] = nested + continue # keep going + + if hasattr(current, "__add__"): + merged_metrics[metric_type] = current.__add__(nested) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue] + assert isinstance(merged_metrics[metric_type], State) # nosec: B101 + continue # keep going + + break # we have multiple value without a way to merge + + return tuple(merged_metrics.values()) + + else: + merged_metric: State | None = self.metrics.get(metric) + for nested in self.nested: + nested_metric: Metric | None = nested.merged(metric) + if nested_metric is None: + continue # skip missing + + if merged_metric is None: + merged_metric = nested_metric + continue # keep going + + if hasattr(merged_metric, "__add__"): + merged_metric = merged_metric.__add__(nested_metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType] + assert isinstance(merged_metric, metric) # nosec: B101 + continue # keep going + + break # we have multiple value without a way to merge + + return cast(Metric | None, merged_metric) + + +@final +class MetricsHolder: + @classmethod + def handler(cls) -> MetricsHandler: + store_handler: Self = cls() + return MetricsHandler( + record=store_handler.record, + read=store_handler.read, + enter_scope=store_handler.enter_scope, + exit_scope=store_handler.exit_scope, + ) + + def __init__(self) -> None: + self.scopes: dict[ScopeIdentifier, MetricsScopeStore] = {} + + def record( + self, + scope: ScopeIdentifier, + /, + metric: State, + ) -> None: + assert scope in self.scopes # nosec: B101 + metric_type: type[State] = type(metric) + metrics: dict[type[State], State] = self.scopes[scope].metrics + if (current := metrics.get(metric_type)) and hasattr(current, "__add__"): + metrics[type(metric)] = current.__add__(metric) # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue] + + metrics[type(metric)] = metric + + async def read[Metric: State]( + self, + scope: ScopeIdentifier, + /, + *, + metric: type[Metric], + merged: bool, + ) -> Metric | None: + if merged: + return self.scopes[scope].merged(metric) - return tuple(merged_metrics.values()) + else: + return cast(Metric | None, self.scopes[scope].metrics.get(metric)) + + def enter_scope[Metric: State]( + self, + scope: ScopeIdentifier, + /, + ) -> None: + assert scope not in self.scopes # nosec: B101 + scope_metrics = MetricsScopeStore(scope) + self.scopes[scope] = scope_metrics + if not scope.is_root: # root scopes have no actual parent + for key in self.scopes.keys(): + if key.scope_id == scope.parent_id: + self.scopes[key].nested.append(scope_metrics) + return + + ctx.log_debug( + "Attempting to enter nested scope metrics without entering its parent first" + ) + + def exit_scope[Metric: State]( + self, + scope: ScopeIdentifier, + /, + ) -> None: + assert scope in self.scopes # nosec: B101 + self.scopes[scope].exited = monotonic() @final @@ -67,6 +172,7 @@ def handler( ) return MetricsHandler( record=logger_handler.record, + read=logger_handler.read, enter_scope=logger_handler.enter_scope, exit_scope=logger_handler.exit_scope, ) @@ -100,6 +206,20 @@ def record( ): ctx.log_debug(f"Recorded metric:\n⎡ {type(metric).__qualname__}:{log}\n⌊") + async def read[Metric: State]( + self, + scope: ScopeIdentifier, + /, + *, + metric: type[Metric], + merged: bool, + ) -> Metric | None: + if merged: + return self.scopes[scope].merged(metric) + + else: + return cast(Metric | None, self.scopes[scope].metrics.get(metric)) + def enter_scope[Metric: State]( self, scope: ScopeIdentifier, @@ -145,6 +265,9 @@ def _tree_log( ) for metric in metrics.merged(): + if type(metric) not in metrics.metrics: + continue # skip metrics not available in this scope + metric_log: str = "" for key, value in vars(metric).items(): if value_log := _value_log( @@ -160,7 +283,7 @@ def _tree_log( if not metric_log: continue # skip empty logs - log += f"\n⎡ •{type(metric).__qualname__}:{metric_log.replace("\n", "\n| ")}\n⌊" + log += f"\n⎡ •{type(metric).__qualname__}:{metric_log.replace('\n', '\n| ')}\n⌊" for nested in metrics.nested: nested_log: str = _tree_log( diff --git a/tests/test_auto_retry.py b/tests/test_auto_retry.py index 39701b4..4d89b56 100644 --- a/tests/test_auto_retry.py +++ b/tests/test_auto_retry.py @@ -61,7 +61,7 @@ def compute(value: str, /) -> str: assert executions == 2 assert logs.output == [ f"ERROR:root:Attempting to retry {compute.__name__}" - f" which failed due to an error: {FakeException("fake")}" + f" which failed due to an error: {FakeException('fake')}" ] @@ -260,7 +260,7 @@ async def compute(value: str, /) -> str: await compute("expected") assert executions == 2 assert logs.output[0].startswith( - f"ERROR:root:Attempting to retry {compute.__name__}" " which failed due to an error" + f"ERROR:root:Attempting to retry {compute.__name__} which failed due to an error" ) diff --git a/uv.lock b/uv.lock index 82d1b44..37e38e6 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ requires-python = ">=3.12" [[package]] name = "bandit" -version = "1.8.0" +version = "1.8.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -11,9 +11,9 @@ dependencies = [ { name = "rich" }, { name = "stevedore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/57/c3/bea54f22cdc8224f0ace18b2cf86c6adf7010285d0ed51b703af9910c5b2/bandit-1.8.0.tar.gz", hash = "sha256:b5bfe55a095abd9fe20099178a7c6c060f844bfd4fe4c76d28e35e4c52b9d31e", size = 4228600 } +sdist = { url = "https://files.pythonhosted.org/packages/9b/e2/c229cdb4eefc124e5b77ac2557eb0a3cb5b9fc89bc465dd2b8dc1033dbb8/bandit-1.8.2.tar.gz", hash = "sha256:e00ad5a6bc676c0954669fe13818024d66b70e42cf5adb971480cf3b671e835f", size = 4228832 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/6b/a9f0574d05d63e7d8125cd02a52732adb6720a9b9f13c921386cb9cdb53e/bandit-1.8.0-py3-none-any.whl", hash = "sha256:b1a61d829c0968aed625381e426aa378904b996529d048f8d908fa28f6b13e38", size = 127035 }, + { url = "https://files.pythonhosted.org/packages/1c/c1/991a7a1404626558cc7db0cc34243e13e5e336eba053bf6979e9fd6006f7/bandit-1.8.2-py3-none-any.whl", hash = "sha256:df6146ad73dd30e8cbda4e29689ddda48364e36ff655dbfc86998401fcf1721f", size = 127049 }, ] [[package]] @@ -65,7 +65,7 @@ wheels = [ [[package]] name = "haiway" -version = "0.8.4" +version = "0.9.0" source = { editable = "." } [package.optional-dependencies] @@ -166,15 +166,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.391" +version = "1.1.392.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/11/05/4ea52a8a45cc28897edb485b4102d37cbfd5fce8445d679cdeb62bfad221/pyright-1.1.391.tar.gz", hash = "sha256:66b2d42cdf5c3cbab05f2f4b76e8bec8aa78e679bfa0b6ad7b923d9e027cadb2", size = 21965 } +sdist = { url = "https://files.pythonhosted.org/packages/66/df/3c6f6b08fba7ccf49b114dfc4bb33e25c299883fd763f93fad47ef8bc58d/pyright-1.1.392.post0.tar.gz", hash = "sha256:3b7f88de74a28dcfa90c7d90c782b6569a48c2be5f9d4add38472bdaac247ebd", size = 3789911 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/89/66f49552fbeb21944c8077d11834b2201514a56fd1b7747ffff9630f1bd9/pyright-1.1.391-py3-none-any.whl", hash = "sha256:54fa186f8b3e8a55a44ebfa842636635688670c6896dcf6cf4a7fc75062f4d15", size = 18579 }, + { url = "https://files.pythonhosted.org/packages/e7/b1/a18de17f40e4f61ca58856b9ef9b0febf74ff88978c3f7776f910071f567/pyright-1.1.392.post0-py3-none-any.whl", hash = "sha256:252f84458a46fa2f0fd4e2f91fc74f50b9ca52c757062e93f6c250c0d8329eb2", size = 5595487 }, ] [[package]] @@ -258,27 +258,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.9.0" +version = "0.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/48/385f276f41e89623a5ea8e4eb9c619a44fdfc2a64849916b3584eca6cb9f/ruff-0.9.0.tar.gz", hash = "sha256:143f68fa5560ecf10fc49878b73cee3eab98b777fcf43b0e62d43d42f5ef9d8b", size = 3489167 } +sdist = { url = "https://files.pythonhosted.org/packages/67/3e/e89f736f01aa9517a97e2e7e0ce8d34a4d8207087b3cfdec95133fee13b5/ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17", size = 3498844 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/01/e0885e5519212efc7ab9d868bc39cb9781931c4c6f9b17becafa81193ec4/ruff-0.9.0-py3-none-linux_armv6l.whl", hash = "sha256:949b3513f931741e006cf267bf89611edff04e1f012013424022add3ce78f319", size = 10647069 }, - { url = "https://files.pythonhosted.org/packages/dd/69/510a9a5781dcf84c2ad513c2003936fefc802f39c745d5f2355d77fa45fd/ruff-0.9.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:99fbcb8c7fe94ae1e462ab2a1ef17cb20b25fb6438b9f198b1bcf5207a0a7916", size = 10401936 }, - { url = "https://files.pythonhosted.org/packages/07/9f/37fb86bfdf28c4cbfe94cbcc01fb9ab0cb8128548f243f34d5298b212562/ruff-0.9.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b022afd8eb0fcfce1e0adec84322abf4d6ce3cd285b3b99c4f17aae7decf749", size = 10010347 }, - { url = "https://files.pythonhosted.org/packages/30/0d/b95121f53c7f7bfb7ba427a35d25f983ed3b476620c5cd69f45caa5b294e/ruff-0.9.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:336567ce92c9ca8ec62780d07b5fa11fbc881dc7bb40958f93a7d621e7ab4589", size = 10882152 }, - { url = "https://files.pythonhosted.org/packages/d4/0b/a955cb6b19eb900c4c594707ab72132ce2d5cd8b5565137fb8fed21b8f08/ruff-0.9.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d338336c44bda602dc8e8766836ac0441e5b0dfeac3af1bd311a97ebaf087a75", size = 10405502 }, - { url = "https://files.pythonhosted.org/packages/1e/fa/9a6c70af74f20edd2519b89eb3322f4bfa399315cf306383443700f2d6b6/ruff-0.9.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9b3ececf523d733e90b540e7afcc0494189e8999847f8855747acd5a9a8c45f", size = 11465069 }, - { url = "https://files.pythonhosted.org/packages/ee/8b/7effac8915470da496be009fe861060baff2692f92801976b2c01cdc8c54/ruff-0.9.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a11c0872a31232e473e2e0e2107f3d294dbadd2f83fb281c3eb1c22a24866924", size = 12176850 }, - { url = "https://files.pythonhosted.org/packages/bd/ed/626179786889eca47b1e821c1582622ac0c1c8f01d60ac974f8b96867a57/ruff-0.9.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5fd06220c17a9cc0dc7fc6552f2ac4db74e8e8bff9c401d160ac59d00566f54", size = 11700963 }, - { url = "https://files.pythonhosted.org/packages/75/79/094c34ddec47fd3c61a0bc5e83ca164344c592949cff91f05961fd40922e/ruff-0.9.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0457e775c74bf3976243f910805242b7dcd389e1d440deccbd1194ca17a5728c", size = 13096560 }, - { url = "https://files.pythonhosted.org/packages/e7/23/ec85dca0dcb329835197401734501bfa1d39e72343df64628c67b72bcbf5/ruff-0.9.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05415599bbcb318f730ea1b46a39e4fbf71f6a63fdbfa1dda92efb55f19d7ecf", size = 11278658 }, - { url = "https://files.pythonhosted.org/packages/6c/17/1b3ea5f06578ea1daa08ac35f9de099d1827eea6e116a8cabbf11235c925/ruff-0.9.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fbf9864b009e43cfc1c8bed1a6a4c529156913105780af4141ca4342148517f5", size = 10879847 }, - { url = "https://files.pythonhosted.org/packages/a6/e5/00bc97d6f419da03c0d898e95cca77311494e7274dc7cc17d94976e32e52/ruff-0.9.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:37b3da222b12e2bb2ce628e02586ab4846b1ed7f31f42a5a0683b213453b2d49", size = 10494220 }, - { url = "https://files.pythonhosted.org/packages/cc/70/d0a23d94f3e40b7ffac0e5506f33bb504672569173781a6c7cab0db6a4ba/ruff-0.9.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:733c0fcf2eb0c90055100b4ed1af9c9d87305b901a8feb6a0451fa53ed88199d", size = 11004182 }, - { url = "https://files.pythonhosted.org/packages/20/8e/367cf8e401890f823d0e4eb33635d0113719d5660b6522b7295376dd95fd/ruff-0.9.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8221a454bfe5ccdf8017512fd6bb60e6ec30f9ea252b8a80e5b73619f6c3cefd", size = 11345761 }, - { url = "https://files.pythonhosted.org/packages/fe/08/4b54e02da73060ebc29368ab15868613f7d2496bde3b01d284d5423646bc/ruff-0.9.0-py3-none-win32.whl", hash = "sha256:d345f2178afd192c7991ddee59155c58145e12ad81310b509bd2e25c5b0247b3", size = 8807005 }, - { url = "https://files.pythonhosted.org/packages/a1/a7/0b422971e897c51bf805f998d75bcfe5d4d858f5002203832875fc91b733/ruff-0.9.0-py3-none-win_amd64.whl", hash = "sha256:0cbc0905d94d21305872f7f8224e30f4bbcd532bc21b2225b2446d8fc7220d19", size = 9689974 }, - { url = "https://files.pythonhosted.org/packages/73/0e/c00f66731e514be3299801b1d9d54efae0abfe8f00a5c14155f2ab9e2920/ruff-0.9.0-py3-none-win_arm64.whl", hash = "sha256:7b1148771c6ca88f820d761350a053a5794bc58e0867739ea93eb5e41ad978cd", size = 9147729 }, + { url = "https://files.pythonhosted.org/packages/dc/05/c3a2e0feb3d5d394cdfd552de01df9d3ec8a3a3771bbff247fab7e668653/ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743", size = 10645241 }, + { url = "https://files.pythonhosted.org/packages/dd/da/59f0a40e5f88ee5c054ad175caaa2319fc96571e1d29ab4730728f2aad4f/ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f", size = 10391066 }, + { url = "https://files.pythonhosted.org/packages/b7/fe/85e1c1acf0ba04a3f2d54ae61073da030f7a5dc386194f96f3c6ca444a78/ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb", size = 10012308 }, + { url = "https://files.pythonhosted.org/packages/6f/9b/780aa5d4bdca8dcea4309264b8faa304bac30e1ce0bcc910422bfcadd203/ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca", size = 10881960 }, + { url = "https://files.pythonhosted.org/packages/12/f4/dac4361afbfe520afa7186439e8094e4884ae3b15c8fc75fb2e759c1f267/ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce", size = 10414803 }, + { url = "https://files.pythonhosted.org/packages/f0/a2/057a3cb7999513cb78d6cb33a7d1cc6401c82d7332583786e4dad9e38e44/ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969", size = 11464929 }, + { url = "https://files.pythonhosted.org/packages/eb/c6/1ccfcc209bee465ced4874dcfeaadc88aafcc1ea9c9f31ef66f063c187f0/ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd", size = 12170717 }, + { url = "https://files.pythonhosted.org/packages/84/97/4a524027518525c7cf6931e9fd3b2382be5e4b75b2b61bec02681a7685a5/ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a", size = 11708921 }, + { url = "https://files.pythonhosted.org/packages/a6/a4/4e77cf6065c700d5593b25fca6cf725b1ab6d70674904f876254d0112ed0/ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b", size = 13058074 }, + { url = "https://files.pythonhosted.org/packages/f9/d6/fcb78e0531e863d0a952c4c5600cc5cd317437f0e5f031cd2288b117bb37/ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831", size = 11281093 }, + { url = "https://files.pythonhosted.org/packages/e4/3b/7235bbeff00c95dc2d073cfdbf2b871b5bbf476754c5d277815d286b4328/ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab", size = 10882610 }, + { url = "https://files.pythonhosted.org/packages/2a/66/5599d23257c61cf038137f82999ca8f9d0080d9d5134440a461bef85b461/ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1", size = 10489273 }, + { url = "https://files.pythonhosted.org/packages/78/85/de4aa057e2532db0f9761e2c2c13834991e087787b93e4aeb5f1cb10d2df/ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366", size = 11003314 }, + { url = "https://files.pythonhosted.org/packages/00/42/afedcaa089116d81447347f76041ff46025849fedb0ed2b187d24cf70fca/ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f", size = 11342982 }, + { url = "https://files.pythonhosted.org/packages/39/c6/fe45f3eb27e3948b41a305d8b768e949bf6a39310e9df73f6c576d7f1d9f/ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72", size = 8819750 }, + { url = "https://files.pythonhosted.org/packages/38/8d/580db77c3b9d5c3d9479e55b0b832d279c30c8f00ab0190d4cd8fc67831c/ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19", size = 9701331 }, + { url = "https://files.pythonhosted.org/packages/b2/94/0498cdb7316ed67a1928300dd87d659c933479f44dec51b4f62bfd1f8028/ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7", size = 9145708 }, ] [[package]]