diff --git a/pyproject.toml b/pyproject.toml index cf139a5..71222e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dev = [ "pytest-asyncio>=0.26.0", "pytest-cov>=6.1.1", "pytest-pretty>=1.3.0", - "ruff>=0.12.0", + "ruff>=0.14.0", ] [build-system] @@ -71,7 +71,7 @@ include = ["src", "tests"] disable_error_code = ["empty-body"] [tool.ruff] -target-version = "py39" +target-version = "py310" [tool.ruff.lint.isort] combine-as-imports = true diff --git a/src/nexusrpc/_common.py b/src/nexusrpc/_common.py index d69aaec..c28650a 100644 --- a/src/nexusrpc/_common.py +++ b/src/nexusrpc/_common.py @@ -2,7 +2,10 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional, TypeVar +from logging import getLogger +from typing import TypeVar + +logger = getLogger(__name__) InputT = TypeVar("InputT", contravariant=True) """Operation input type""" @@ -32,14 +35,14 @@ class HandlerError(Exception): # Raise a bad request error raise nexusrpc.HandlerError( "Invalid input provided", - type=nexusrpc.HandlerErrorType.BAD_REQUEST + error_type=nexusrpc.HandlerErrorType.BAD_REQUEST ) # Raise a retryable internal error raise nexusrpc.HandlerError( "Database unavailable", - type=nexusrpc.HandlerErrorType.INTERNAL, - retryable=True + error_type=nexusrpc.HandlerErrorType.INTERNAL, + retryable_override=True ) """ @@ -47,8 +50,8 @@ def __init__( self, message: str, *, - type: HandlerErrorType, - retryable_override: Optional[bool] = None, + error_type: HandlerErrorType | str, + retryable_override: bool | None = None, ): """ Initialize a new HandlerError. @@ -56,22 +59,31 @@ def __init__( :param message: A descriptive message for the error. This will become the `message` in the resulting Nexus Failure object. - :param type: The :py:class:`HandlerErrorType` of the error. + :param error_type: The :py:class:`HandlerErrorType` of the error, or a + string representation of the error type. If a string is + provided and doesn't match a known error type, it will + be treated as UNKNOWN and a warning will be logged. :param retryable_override: Optionally set whether the error should be retried. By default, the error type is used to determine this. """ super().__init__(message) - self._type = type - self._retryable_override = retryable_override - @property - def retryable_override(self) -> Optional[bool]: - """ - The optional retryability override set when this error was created. - """ - return self._retryable_override + # Handle string error types + if isinstance(error_type, str): + raw_error_type = error_type + try: + error_type = HandlerErrorType[error_type] + except KeyError: + logger.warning(f"Unknown Nexus HandlerErrorType: {error_type}") + error_type = HandlerErrorType.UNKNOWN + else: + raw_error_type = error_type.value + + self.error_type = error_type + self.raw_error_type = raw_error_type + self.retryable_override = retryable_override @property def retryable(self) -> bool: @@ -82,40 +94,28 @@ def retryable(self) -> bool: error type is used. See https://github.com/nexus-rpc/api/blob/main/SPEC.md#predefined-handler-errors """ - if self._retryable_override is not None: - return self._retryable_override - - non_retryable_types = { - HandlerErrorType.BAD_REQUEST, - HandlerErrorType.UNAUTHENTICATED, - HandlerErrorType.UNAUTHORIZED, - HandlerErrorType.NOT_FOUND, - HandlerErrorType.CONFLICT, - HandlerErrorType.NOT_IMPLEMENTED, - } - retryable_types = { - HandlerErrorType.REQUEST_TIMEOUT, - HandlerErrorType.RESOURCE_EXHAUSTED, - HandlerErrorType.INTERNAL, - HandlerErrorType.UNAVAILABLE, - HandlerErrorType.UPSTREAM_TIMEOUT, - } - if self._type in non_retryable_types: - return False - elif self._type in retryable_types: - return True - else: - return True - - @property - def type(self) -> HandlerErrorType: - """ - The type of handler error. - - See :py:class:`HandlerErrorType` and - https://github.com/nexus-rpc/api/blob/main/SPEC.md#predefined-handler-errors. - """ - return self._type + if self.retryable_override is not None: + return self.retryable_override + + match self.error_type: + case ( + HandlerErrorType.BAD_REQUEST + | HandlerErrorType.UNAUTHENTICATED + | HandlerErrorType.UNAUTHORIZED + | HandlerErrorType.NOT_FOUND + | HandlerErrorType.CONFLICT + | HandlerErrorType.NOT_IMPLEMENTED + ): + return False + case ( + HandlerErrorType.RESOURCE_EXHAUSTED + | HandlerErrorType.REQUEST_TIMEOUT + | HandlerErrorType.INTERNAL + | HandlerErrorType.UNAVAILABLE + | HandlerErrorType.UPSTREAM_TIMEOUT + | HandlerErrorType.UNKNOWN + ): + return True class HandlerErrorType(Enum): @@ -124,6 +124,11 @@ class HandlerErrorType(Enum): See https://github.com/nexus-rpc/api/blob/main/SPEC.md#predefined-handler-errors """ + UNKNOWN = "UNKNOWN" + """ + The error type is unknown. Subsequent requests by the client are permissible. + """ + BAD_REQUEST = "BAD_REQUEST" """ The handler cannot or will not process the request due to an apparent client error. diff --git a/src/nexusrpc/handler/_core.py b/src/nexusrpc/handler/_core.py index eb3b01a..b67dfa4 100644 --- a/src/nexusrpc/handler/_core.py +++ b/src/nexusrpc/handler/_core.py @@ -208,7 +208,7 @@ def _get_service_handler(self, service_name: str) -> ServiceHandler: if service is None: raise HandlerError( f"No handler for service '{service_name}'.", - type=HandlerErrorType.NOT_FOUND, + error_type=HandlerErrorType.NOT_FOUND, ) return service @@ -376,7 +376,7 @@ def get_operation_handler(self, operation_name: str) -> OperationHandler[Any, An f"Nexus service definition '{self.service.name}' has no operation " f"'{operation_name}'. There are {len(self.service.operation_definitions)} operations " f"in the definition.", - type=HandlerErrorType.NOT_FOUND, + error_type=HandlerErrorType.NOT_FOUND, ) operation_handler = self.operation_handlers.get(operation_name) if operation_handler is None: @@ -384,7 +384,7 @@ def get_operation_handler(self, operation_name: str) -> OperationHandler[Any, An f"Nexus service implementation '{self.service.name}' has no handler for " f"operation '{operation_name}'. There are {len(self.operation_handlers)} " f"available operation handlers.", - type=HandlerErrorType.NOT_FOUND, + error_type=HandlerErrorType.NOT_FOUND, ) return operation_handler @@ -416,7 +416,7 @@ class OperationHandlerMiddleware(ABC): """ Middleware for operation handlers. - This should be extended by any operation handler middelware. + This should be extended by any operation handler middleware. """ @abstractmethod diff --git a/tests/test_common.py b/tests/test_common.py index 11e0abb..4df1f9e 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -3,39 +3,69 @@ def test_handler_error_retryable_type(): retryable_error_type = HandlerErrorType.RESOURCE_EXHAUSTED - assert HandlerError( + err = HandlerError( "test", - type=retryable_error_type, + error_type=retryable_error_type, retryable_override=True, - ).retryable + ) + assert err.retryable + assert err.error_type == retryable_error_type + assert err.raw_error_type == retryable_error_type.value - assert not HandlerError( + err = HandlerError( "test", - type=retryable_error_type, + error_type=retryable_error_type, retryable_override=False, - ).retryable + ) + assert not err.retryable + assert err.error_type == retryable_error_type + assert err.raw_error_type == retryable_error_type.value - assert HandlerError( + err = HandlerError( "test", - type=retryable_error_type, - ).retryable + error_type=retryable_error_type, + ) + assert err.retryable + assert err.error_type == retryable_error_type + assert err.raw_error_type == retryable_error_type.value def test_handler_error_non_retryable_type(): non_retryable_error_type = HandlerErrorType.BAD_REQUEST - assert HandlerError( + err = HandlerError( "test", - type=non_retryable_error_type, + error_type=non_retryable_error_type, retryable_override=True, - ).retryable + ) + assert err.retryable + assert err.error_type == non_retryable_error_type + assert err.raw_error_type == non_retryable_error_type.value - assert not HandlerError( + err = HandlerError( "test", - type=non_retryable_error_type, + error_type=non_retryable_error_type, retryable_override=False, - ).retryable + ) + assert not err.retryable + assert err.error_type == non_retryable_error_type + assert err.raw_error_type == non_retryable_error_type.value - assert not HandlerError( + err = HandlerError( "test", - type=non_retryable_error_type, - ).retryable + error_type=non_retryable_error_type, + ) + assert not err.retryable + assert err.error_type == non_retryable_error_type + assert err.raw_error_type == non_retryable_error_type.value + + +def test_handler_error_unknown_error_type(): + err = HandlerError("test", error_type="SOME_UNKNOWN_TYPE") + assert err.retryable + assert err.error_type == HandlerErrorType.UNKNOWN + assert err.raw_error_type == "SOME_UNKNOWN_TYPE" + + err = HandlerError("test", error_type="SOME_UNKNOWN_TYPE", retryable_override=False) + assert not err.retryable + assert err.error_type == HandlerErrorType.UNKNOWN + assert err.raw_error_type == "SOME_UNKNOWN_TYPE" diff --git a/uv.lock b/uv.lock index 08553f3..5819903 100644 --- a/uv.lock +++ b/uv.lock @@ -451,7 +451,7 @@ dev = [ { name = "pytest-asyncio", specifier = ">=0.26.0" }, { name = "pytest-cov", specifier = ">=6.1.1" }, { name = "pytest-pretty", specifier = ">=1.3.0" }, - { name = "ruff", specifier = ">=0.12.0" }, + { name = "ruff", specifier = ">=0.14.0" }, ] [[package]] @@ -702,27 +702,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, - { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, - { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, - { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, - { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, - { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, - { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, - { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, - { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, ] [[package]]