From 16652c3d4b284ba8e6347ef157c5029aba6d9d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 11:40:34 +0100 Subject: [PATCH 01/15] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Better=20PostgreSQL?= =?UTF-8?q?=20URL=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grelmicro/sync/postgres.py | 70 +++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/grelmicro/sync/postgres.py b/grelmicro/sync/postgres.py index 35b32a1..8614e89 100644 --- a/grelmicro/sync/postgres.py +++ b/grelmicro/sync/postgres.py @@ -23,33 +23,43 @@ class _PostgresSettings(BaseSettings): POSTGRES_PASSWORD: str | None = None POSTGRES_URL: PostgresDsn | None = None - def url(self) -> str: - """Generate the Postgres URL from the parts.""" - if self.POSTGRES_URL: - return self.POSTGRES_URL.unicode_string() - - if all( - ( - self.POSTGRES_HOST, - self.POSTGRES_DB, - self.POSTGRES_USER, - self.POSTGRES_PASSWORD, - ) - ): - return MultiHostUrl.build( - scheme="postgresql", - username=self.POSTGRES_USER, - password=self.POSTGRES_PASSWORD, - host=self.POSTGRES_HOST, - port=self.POSTGRES_PORT, - path=self.POSTGRES_DB, - ).unicode_string() - - msg = ( - "Either POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and " - "POSTGRES_PASSWORD must be set" - ) - raise SyncSettingsValidationError(msg) + +def _get_postgres_url() -> str: + """Get the PostgreSQL URL from the environment variables. + + Raises: + SyncSettingsValidationError: If the URL or all of the host, database, user, and password + """ + try: + settings = _PostgresSettings() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + parts_fields = [ + settings.POSTGRES_HOST, + settings.POSTGRES_DB, + settings.POSTGRES_USER, + settings.POSTGRES_PASSWORD, + ] + + if settings.POSTGRES_URL and not any(parts_fields): + return settings.POSTGRES_URL.unicode_string() + + if all(parts_fields) and not settings.POSTGRES_URL: + return MultiHostUrl.build( + scheme="postgresql", + username=settings.POSTGRES_USER, + password=settings.POSTGRES_PASSWORD, + host=settings.POSTGRES_HOST, + port=settings.POSTGRES_PORT, + path=f"/{settings.POSTGRES_DB}", + ).unicode_string() + + msg = ( + "Either POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and " + "POSTGRES_PASSWORD must be set" + ) + raise SyncSettingsValidationError(msg) class PostgresSyncBackend(SyncBackend): @@ -120,11 +130,7 @@ def __init__( msg = f"Table name '{table_name}' is not a valid identifier" raise ValueError(msg) - try: - self._url = url or _PostgresSettings().url() - except ValidationError as error: - raise SyncSettingsValidationError(error) from None - + self._url = url or _get_postgres_url() self._table_name = table_name self._acquire_sql = self._SQL_ACQUIRE_OR_EXTEND.format( table_name=table_name From e62bdd1d38977940db36946450f13bfcd42bb172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 12:04:17 +0100 Subject: [PATCH 02/15] =?UTF-8?q?=E2=9C=A8=20Add=20Redis=20settings=20mana?= =?UTF-8?q?gement=20and=20validation=20from=20environment=20variables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grelmicro/sync/redis.py | 56 ++++++++++++++++++++++++++++++--- tests/sync/test_redis.py | 67 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 tests/sync/test_redis.py diff --git a/grelmicro/sync/redis.py b/grelmicro/sync/redis.py index 9f6b494..73090c8 100644 --- a/grelmicro/sync/redis.py +++ b/grelmicro/sync/redis.py @@ -3,12 +3,52 @@ from types import TracebackType from typing import Annotated, Self -from pydantic import RedisDsn +from pydantic import RedisDsn, ValidationError +from pydantic_core import Url +from pydantic_settings import BaseSettings from redis.asyncio.client import Redis from typing_extensions import Doc from grelmicro.sync._backends import loaded_backends from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import SyncSettingsValidationError + + +class _RedisSettings(BaseSettings): + """Redis settings from the environment variables.""" + + REDIS_HOST: str | None = None + REDIS_PORT: int = 6379 + REDIS_DB: int = 0 + REDIS_PASSWORD: str | None = None + REDIS_URL: RedisDsn | None = None + + +def _get_redis_url() -> str: + """Get the Redis URL from the environment variables. + + Raises: + SyncSettingsValidationError: If the URL or host is not set. + """ + try: + settings = _RedisSettings() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + if settings.REDIS_URL and not settings.REDIS_HOST: + return settings.REDIS_URL.unicode_string() + + if settings.REDIS_HOST and not settings.REDIS_URL: + return Url.build( + scheme="redis", + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + path=str(settings.REDIS_DB), + password=settings.REDIS_PASSWORD, + ).unicode_string() + + msg = "Either REDIS_URL or REDIS_HOST must be set" + raise SyncSettingsValidationError(msg) class RedisSyncBackend(SyncBackend): @@ -37,7 +77,15 @@ class RedisSyncBackend(SyncBackend): def __init__( self, - url: Annotated[RedisDsn | str, Doc("The Redis database URL.")], + url: Annotated[ + RedisDsn | str | None, + Doc(""" + The Redis URL. + + If not provided, the URL will be taken from the environment variables REDIS_URL + or REDIS_HOST, REDIS_PORT, REDIS_DB, and REDIS_PASSWORD. + """), + ] = None, *, auto_register: Annotated[ bool, @@ -47,8 +95,8 @@ def __init__( ] = True, ) -> None: """Initialize the lock backend.""" - self._url = url - self._redis: Redis = Redis.from_url(str(url)) + self._url = url or _get_redis_url() + self._redis: Redis = Redis.from_url(str(self._url)) self._lua_release = self._redis.register_script(self._LUA_RELEASE) self._lua_acquire = self._redis.register_script( self._LUA_ACQUIRE_OR_EXTEND diff --git a/tests/sync/test_redis.py b/tests/sync/test_redis.py new file mode 100644 index 0000000..a14bad7 --- /dev/null +++ b/tests/sync/test_redis.py @@ -0,0 +1,67 @@ +"""Tests for Redis Backends.""" + +import pytest + +from grelmicro.sync.errors import SyncSettingsValidationError +from grelmicro.sync.redis import RedisSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +URL = "redis://:test_password@test_host:1234/0" + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": URL}, + { + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Act + backend = RedisSyncBackend() + + # Assert + assert backend._url == URL + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": "test://:test_password@test_host:1234/0"}, + {"REDIS_PASSWORD": "test_password"}, + { + "REDIS_URL": "test://:test_password@test_host:1234/0", + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings_validation_error( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Assert / Act + with pytest.raises( + SyncSettingsValidationError, + match=(r"Could not validate environment variables settings:\n"), + ): + RedisSyncBackend() From 072c87e46d892c1c85cc30c9b8345816dc8096d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 12:04:24 +0100 Subject: [PATCH 03/15] =?UTF-8?q?=F0=9F=9A=80=20Bump=20grelmicro=20version?= =?UTF-8?q?=20to=200.2.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 5e81fe5..ff11a2b 100644 --- a/uv.lock +++ b/uv.lock @@ -409,7 +409,7 @@ wheels = [ [[package]] name = "grelmicro" -version = "0.2.1" +version = "0.2.2" source = { editable = "." } dependencies = [ { name = "anyio" }, From 74b214b0eeae9c6b0ebef466b1cfe173c186f025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 12:05:39 +0100 Subject: [PATCH 04/15] =?UTF-8?q?=F0=9F=8E=A8=20Enhance=20PostgreSQL=20set?= =?UTF-8?q?tings=20with=20environment=20variable=20documentation=20and=20i?= =?UTF-8?q?mprove=20URL=20generation=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grelmicro/sync/postgres.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/grelmicro/sync/postgres.py b/grelmicro/sync/postgres.py index 8614e89..451cc0c 100644 --- a/grelmicro/sync/postgres.py +++ b/grelmicro/sync/postgres.py @@ -16,6 +16,8 @@ class _PostgresSettings(BaseSettings): + """PostgreSQL settings from the environment variables.""" + POSTGRES_HOST: str | None = None POSTGRES_PORT: int = 5432 POSTGRES_DB: str | None = None @@ -35,24 +37,24 @@ def _get_postgres_url() -> str: except ValidationError as error: raise SyncSettingsValidationError(error) from None - parts_fields = [ + required_parts = [ settings.POSTGRES_HOST, settings.POSTGRES_DB, settings.POSTGRES_USER, settings.POSTGRES_PASSWORD, ] - if settings.POSTGRES_URL and not any(parts_fields): + if settings.POSTGRES_URL and not any(required_parts): return settings.POSTGRES_URL.unicode_string() - if all(parts_fields) and not settings.POSTGRES_URL: + if all(required_parts) and not settings.POSTGRES_URL: return MultiHostUrl.build( scheme="postgresql", username=settings.POSTGRES_USER, password=settings.POSTGRES_PASSWORD, host=settings.POSTGRES_HOST, port=settings.POSTGRES_PORT, - path=f"/{settings.POSTGRES_DB}", + path=settings.POSTGRES_DB, ).unicode_string() msg = ( From 6d915319f3c6430376dcb459815d4d9a8fcc060d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 12:06:21 +0100 Subject: [PATCH 05/15] =?UTF-8?q?=E2=9C=85=20Improve=20PostgreSQL=20test?= =?UTF-8?q?=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/sync/test_postgres.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/sync/test_postgres.py b/tests/sync/test_postgres.py index ef8dd18..dc933b8 100644 --- a/tests/sync/test_postgres.py +++ b/tests/sync/test_postgres.py @@ -8,7 +8,7 @@ pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] -URL = "postgres://user:password@localhost:5432/db" +URL = "postgresql://test_user:test_password@test_host:1234/test_db" @pytest.mark.parametrize( @@ -51,9 +51,7 @@ async def test_sync_backend_out_of_context_errors() -> None: @pytest.mark.parametrize( ("environs"), [ - { - "POSTGRES_URL": "postgresql://test_user:test_password@test_host:1234/test_db" - }, + {"POSTGRES_URL": URL}, { "POSTGRES_USER": "test_user", "POSTGRES_PASSWORD": "test_password", @@ -75,10 +73,7 @@ def test_postgres_env_var_settings( backend = PostgresSyncBackend() # Assert - assert ( - backend._url - == "postgresql://test_user:test_password@test_host:1234/test_db" - ) + assert backend._url == URL @pytest.mark.parametrize( @@ -88,6 +83,14 @@ def test_postgres_env_var_settings( "POSTGRES_URL": "test://test_user:test_password@test_host:1234/test_db" }, {"POSTGRES_USER": "test_user"}, + { + "POSTGRES_URL": URL, + "POSTGRES_USER": "test_user", + "POSTGRES_PASSWORD": "test_password", + "POSTGRES_HOST": "test_host", + "POSTGRES_PORT": "1234", + "POSTGRES_DB": "test_db", + }, ], ) def test_postgres_env_var_settings_validation_error( From d98ba27fd78cb626956429386cd7b4174a5854aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sat, 30 Nov 2024 14:10:26 +0100 Subject: [PATCH 06/15] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20pytest=20config?= =?UTF-8?q?uration=20for=20unit=20and=20integration=20testing,=20and=20add?= =?UTF-8?q?=20coverage=20report=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 23 +++++++++++++++++++---- .vscode/settings.json | 2 +- pyproject.toml | 2 -- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e5a141..c944d92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,14 +50,29 @@ repos: types: [python] require_serial: true - - id: pytest - name: pytest + - id: pytest-unit + name: pytest-unit description: "Run 'pytest' for unit testing" - entry: uv run pytest --cov-fail-under=90 + entry: uv run pytest -m "not integration" language: system pass_filenames: false + - id: pytest-integration + name: pytest-integration + description: "Run 'pytest' for integration testing" + entry: uv run pytest -m "integration" --cov-append + language: system + pass_filenames: false + + - id: coverage-report + name: coverage-report + description: "Generate coverage report" + entry: uv run coverage report --fail-under=100 + language: system + pass_filenames: false + + ci: autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate - skip: [uv-lock, mypy, pytest] + skip: [uv-lock, mypy, pytest-unit, pytest-integration] diff --git a/.vscode/settings.json b/.vscode/settings.json index 806ffc4..01e3b38 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,7 +23,7 @@ "python.terminal.activateEnvironment": true, "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, - "python.testing.pytestArgs": ["--no-cov", "--color=yes"], + "python.testing.pytestArgs": ["--color=yes"], "python.analysis.inlayHints.pytestParameters": true, // Python editor settings diff --git a/pyproject.toml b/pyproject.toml index 9bcca87..16cf486 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -153,11 +153,9 @@ disallow_untyped_defs = false [tool.pytest.ini_options] addopts = """ --cov=grelmicro - --cov-report term:skip-covered --cov-report xml:cov.xml --strict-config --strict-markers - -m "not integration" """ markers = """ integration: mark a test as an integration test (disabled by default). From d09c7aabae0c773e37737f778b5d78e1fe0bea9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sat, 30 Nov 2024 14:10:40 +0100 Subject: [PATCH 07/15] =?UTF-8?q?=E2=9C=A8=20Add=20prefix=20support=20for?= =?UTF-8?q?=20Redis=20keys=20to=20avoid=20conflicts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grelmicro/sync/redis.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/grelmicro/sync/redis.py b/grelmicro/sync/redis.py index 73090c8..b763f23 100644 --- a/grelmicro/sync/redis.py +++ b/grelmicro/sync/redis.py @@ -87,6 +87,14 @@ def __init__( """), ] = None, *, + prefix: Annotated[ + str, + Doc(""" + The prefix to add on redis keys to avoid conflicts with other keys. + + By default no prefix is added. + """), + ] = "", auto_register: Annotated[ bool, Doc( @@ -97,6 +105,7 @@ def __init__( """Initialize the lock backend.""" self._url = url or _get_redis_url() self._redis: Redis = Redis.from_url(str(self._url)) + self._prefix = prefix self._lua_release = self._redis.register_script(self._LUA_RELEASE) self._lua_acquire = self._redis.register_script( self._LUA_ACQUIRE_OR_EXTEND @@ -121,7 +130,7 @@ async def acquire(self, *, name: str, token: str, duration: float) -> bool: """Acquire the lock.""" return bool( await self._lua_acquire( - keys=[name], + keys=[f"{self._prefix}{name}"], args=[token, int(duration * 1000)], client=self._redis, ) @@ -131,16 +140,16 @@ async def release(self, *, name: str, token: str) -> bool: """Release the lock.""" return bool( await self._lua_release( - keys=[name], args=[token], client=self._redis + keys=[f"{self._prefix}{name}"], args=[token], client=self._redis ) ) async def locked(self, *, name: str) -> bool: """Check if the lock is acquired.""" - return bool(await self._redis.get(name)) + return bool(await self._redis.get(f"{self._prefix}{name}")) async def owned(self, *, name: str, token: str) -> bool: """Check if the lock is owned.""" return bool( - (await self._redis.get(name)) == token.encode() + (await self._redis.get(f"{self._prefix}{name}")) == token.encode() ) # redis returns bytes From 797fb30609bb6a013d3b98a1988daff8ce6c6f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 12:04:17 +0100 Subject: [PATCH 08/15] =?UTF-8?q?=E2=9C=A8=20Add=20Redis=20settings=20mana?= =?UTF-8?q?gement=20and=20validation=20from=20environment=20variables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grelmicro/sync/redis.py | 56 ++++++++++++++++++++++++++++++--- tests/sync/test_redis.py | 67 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 tests/sync/test_redis.py diff --git a/grelmicro/sync/redis.py b/grelmicro/sync/redis.py index 9f6b494..73090c8 100644 --- a/grelmicro/sync/redis.py +++ b/grelmicro/sync/redis.py @@ -3,12 +3,52 @@ from types import TracebackType from typing import Annotated, Self -from pydantic import RedisDsn +from pydantic import RedisDsn, ValidationError +from pydantic_core import Url +from pydantic_settings import BaseSettings from redis.asyncio.client import Redis from typing_extensions import Doc from grelmicro.sync._backends import loaded_backends from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import SyncSettingsValidationError + + +class _RedisSettings(BaseSettings): + """Redis settings from the environment variables.""" + + REDIS_HOST: str | None = None + REDIS_PORT: int = 6379 + REDIS_DB: int = 0 + REDIS_PASSWORD: str | None = None + REDIS_URL: RedisDsn | None = None + + +def _get_redis_url() -> str: + """Get the Redis URL from the environment variables. + + Raises: + SyncSettingsValidationError: If the URL or host is not set. + """ + try: + settings = _RedisSettings() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + if settings.REDIS_URL and not settings.REDIS_HOST: + return settings.REDIS_URL.unicode_string() + + if settings.REDIS_HOST and not settings.REDIS_URL: + return Url.build( + scheme="redis", + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + path=str(settings.REDIS_DB), + password=settings.REDIS_PASSWORD, + ).unicode_string() + + msg = "Either REDIS_URL or REDIS_HOST must be set" + raise SyncSettingsValidationError(msg) class RedisSyncBackend(SyncBackend): @@ -37,7 +77,15 @@ class RedisSyncBackend(SyncBackend): def __init__( self, - url: Annotated[RedisDsn | str, Doc("The Redis database URL.")], + url: Annotated[ + RedisDsn | str | None, + Doc(""" + The Redis URL. + + If not provided, the URL will be taken from the environment variables REDIS_URL + or REDIS_HOST, REDIS_PORT, REDIS_DB, and REDIS_PASSWORD. + """), + ] = None, *, auto_register: Annotated[ bool, @@ -47,8 +95,8 @@ def __init__( ] = True, ) -> None: """Initialize the lock backend.""" - self._url = url - self._redis: Redis = Redis.from_url(str(url)) + self._url = url or _get_redis_url() + self._redis: Redis = Redis.from_url(str(self._url)) self._lua_release = self._redis.register_script(self._LUA_RELEASE) self._lua_acquire = self._redis.register_script( self._LUA_ACQUIRE_OR_EXTEND diff --git a/tests/sync/test_redis.py b/tests/sync/test_redis.py new file mode 100644 index 0000000..a14bad7 --- /dev/null +++ b/tests/sync/test_redis.py @@ -0,0 +1,67 @@ +"""Tests for Redis Backends.""" + +import pytest + +from grelmicro.sync.errors import SyncSettingsValidationError +from grelmicro.sync.redis import RedisSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +URL = "redis://:test_password@test_host:1234/0" + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": URL}, + { + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Act + backend = RedisSyncBackend() + + # Assert + assert backend._url == URL + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": "test://:test_password@test_host:1234/0"}, + {"REDIS_PASSWORD": "test_password"}, + { + "REDIS_URL": "test://:test_password@test_host:1234/0", + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings_validation_error( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Assert / Act + with pytest.raises( + SyncSettingsValidationError, + match=(r"Could not validate environment variables settings:\n"), + ): + RedisSyncBackend() From ec7ee223a91dd01e44799aceed7a7f0f3cabcf69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 12:04:24 +0100 Subject: [PATCH 09/15] =?UTF-8?q?=F0=9F=9A=80=20Bump=20grelmicro=20version?= =?UTF-8?q?=20to=200.2.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 5e81fe5..ff11a2b 100644 --- a/uv.lock +++ b/uv.lock @@ -409,7 +409,7 @@ wheels = [ [[package]] name = "grelmicro" -version = "0.2.1" +version = "0.2.2" source = { editable = "." } dependencies = [ { name = "anyio" }, From 378657362ee46fe02476cb429a9976ea4a34d37b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 12:05:39 +0100 Subject: [PATCH 10/15] =?UTF-8?q?=F0=9F=8E=A8=20Enhance=20PostgreSQL=20set?= =?UTF-8?q?tings=20with=20environment=20variable=20documentation=20and=20i?= =?UTF-8?q?mprove=20URL=20generation=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.github}/workflows/ci.yml | 0 .../.github}/workflows/release.yml | 0 .gitignore => .auto-resolution/.gitignore | 0 .../.pre-commit-config.yaml | 0 .../.vscode}/settings.json | 0 LICENSE => .auto-resolution/LICENSE | 0 README.md => .auto-resolution/README.md | 0 {docs => .auto-resolution/docs}/index.md | 0 {docs => .auto-resolution/docs}/logging.md | 0 {docs => .auto-resolution/docs}/sync.md | 0 {docs => .auto-resolution/docs}/task.md | 0 .../examples}/__init__.py | 0 .../examples}/logging/__init__.py | 0 .../examples}/logging/basic.log | 0 .../examples}/logging/basic.py | 0 .../examples}/logging/configure_logging.py | 0 .../examples}/logging/fastapi.py | 0 .../examples}/simple_fastapi_app.py | 0 .../examples}/single_file_app.py | 0 .../examples}/sync/__init__.py | 0 .../examples}/sync/leaderelection_anyio.py | 0 .../examples}/sync/leaderelection_task.py | 0 .../examples}/sync/lock.py | 0 .../examples}/sync/memory.py | 0 .../examples}/sync/postgres.py | 0 .../examples}/sync/redis.py | 0 .../examples}/task/__init__.py | 0 .../examples}/task/fastapi.py | 0 .../examples}/task/faststream.py | 0 .../examples}/task/interval_manager.py | 0 .../examples}/task/interval_router.py | 0 .../examples}/task/leaderelection.py | 0 .../examples}/task/lock.py | 0 .../examples}/task/router.py | 0 .../grelmicro}/__init__.py | 0 .../grelmicro}/errors.py | 0 .../grelmicro}/logging/__init__.py | 0 .../grelmicro}/logging/config.py | 0 .../grelmicro}/logging/errors.py | 0 .../grelmicro}/logging/loguru.py | 0 .../grelmicro}/py.typed | 0 .../grelmicro}/sync/__init__.py | 0 .../grelmicro}/sync/_backends.py | 0 .../grelmicro}/sync/_base.py | 0 .../grelmicro}/sync/_utils.py | 0 .../grelmicro}/sync/abc.py | 0 .../grelmicro}/sync/errors.py | 0 .../grelmicro}/sync/leaderelection.py | 0 .../grelmicro}/sync/lock.py | 0 .../grelmicro}/sync/memory.py | 0 .../grelmicro}/sync/postgres.py | 0 .../grelmicro}/sync/redis.py | 0 .../grelmicro}/task/__init__.py | 0 .../grelmicro}/task/_interval.py | 0 .../grelmicro}/task/_utils.py | 0 .../grelmicro}/task/abc.py | 0 .../grelmicro}/task/errors.py | 0 .../grelmicro}/task/manager.py | 0 .../grelmicro}/task/router.py | 0 mkdocs.yml => .auto-resolution/mkdocs.yml | 0 .../pyproject.toml | 0 {tests => .auto-resolution/tests}/__init__.py | 0 {tests => .auto-resolution/tests}/conftest.py | 0 .../tests}/logging/__init__.py | 0 .../tests}/logging/test_loguru.py | 0 .../tests}/sync/__init__.py | 0 .../tests}/sync/test_backends.py | 0 .../tests}/sync/test_leaderelection.py | 0 .../tests}/sync/test_lock.py | 0 .../tests}/sync/test_postgres.py | 0 .../tests}/sync/test_redis.py | 0 .../tests}/sync/utils.py | 0 .../tests}/task/__init__.py | 0 .../tests}/task/samples.py | 0 .../tests}/task/test_interval.py | 0 .../tests}/task/test_manager.py | 0 .../tests}/task/test_router.py | 0 uv.lock => .auto-resolution/uv.lock | 0 .conflict-base-0/.github/workflows/ci.yml | 94 + .../.github/workflows/release.yml | 110 + .conflict-base-0/.gitignore | 17 + .conflict-base-0/.pre-commit-config.yaml | 63 + .conflict-base-0/.vscode/settings.json | 58 + .conflict-base-0/LICENSE | 21 + .conflict-base-0/README.md | 158 ++ .conflict-base-0/docs/index.md | 158 ++ .conflict-base-0/docs/logging.md | 73 + .conflict-base-0/docs/sync.md | 81 + .conflict-base-0/docs/task.md | 85 + .conflict-base-0/examples/__init__.py | 1 + .conflict-base-0/examples/logging/__init__.py | 1 + .conflict-base-0/examples/logging/basic.log | 4 + .conflict-base-0/examples/logging/basic.py | 17 + .../examples/logging/configure_logging.py | 3 + .conflict-base-0/examples/logging/fastapi.py | 22 + .../examples/simple_fastapi_app.py | 54 + .conflict-base-0/examples/single_file_app.py | 114 + .conflict-base-0/examples/sync/__init__.py | 1 + .../examples/sync/leaderelection_anyio.py | 11 + .../examples/sync/leaderelection_task.py | 6 + .conflict-base-0/examples/sync/lock.py | 8 + .conflict-base-0/examples/sync/memory.py | 3 + .conflict-base-0/examples/sync/postgres.py | 3 + .conflict-base-0/examples/sync/redis.py | 3 + .conflict-base-0/examples/task/__init__.py | 1 + .conflict-base-0/examples/task/fastapi.py | 16 + .conflict-base-0/examples/task/faststream.py | 18 + .../examples/task/interval_manager.py | 8 + .../examples/task/interval_router.py | 8 + .../examples/task/leaderelection.py | 12 + .conflict-base-0/examples/task/lock.py | 11 + .conflict-base-0/examples/task/router.py | 15 + .conflict-base-0/grelmicro/__init__.py | 3 + .conflict-base-0/grelmicro/errors.py | 52 + .../grelmicro/logging/__init__.py | 5 + .conflict-base-0/grelmicro/logging/config.py | 43 + .conflict-base-0/grelmicro/logging/errors.py | 7 + .conflict-base-0/grelmicro/logging/loguru.py | 121 ++ .conflict-base-0/grelmicro/py.typed | 0 .conflict-base-0/grelmicro/sync/__init__.py | 6 + .conflict-base-0/grelmicro/sync/_backends.py | 30 + .conflict-base-0/grelmicro/sync/_base.py | 101 + .conflict-base-0/grelmicro/sync/_utils.py | 38 + .conflict-base-0/grelmicro/sync/abc.py | 106 + .conflict-base-0/grelmicro/sync/errors.py | 67 + .../grelmicro/sync/leaderelection.py | 386 ++++ .conflict-base-0/grelmicro/sync/lock.py | 324 +++ .conflict-base-0/grelmicro/sync/memory.py | 78 + .conflict-base-0/grelmicro/sync/postgres.py | 204 ++ .conflict-base-0/grelmicro/sync/redis.py | 146 ++ .conflict-base-0/grelmicro/task/__init__.py | 6 + .conflict-base-0/grelmicro/task/_interval.py | 92 + .conflict-base-0/grelmicro/task/_utils.py | 43 + .conflict-base-0/grelmicro/task/abc.py | 31 + .conflict-base-0/grelmicro/task/errors.py | 28 + .conflict-base-0/grelmicro/task/manager.py | 89 + .conflict-base-0/grelmicro/task/router.py | 132 ++ .conflict-base-0/mkdocs.yml | 47 + .conflict-base-0/pyproject.toml | 174 ++ .conflict-base-0/tests/__init__.py | 1 + .conflict-base-0/tests/conftest.py | 9 + .conflict-base-0/tests/logging/__init__.py | 1 + .conflict-base-0/tests/logging/test_loguru.py | 274 +++ .conflict-base-0/tests/sync/__init__.py | 1 + .conflict-base-0/tests/sync/test_backends.py | 370 ++++ .../tests/sync/test_leaderelection.py | 457 ++++ .conflict-base-0/tests/sync/test_lock.py | 506 +++++ .conflict-base-0/tests/sync/test_postgres.py | 106 + .conflict-base-0/tests/sync/test_redis.py | 67 + .conflict-base-0/tests/sync/utils.py | 23 + .conflict-base-0/tests/task/__init__.py | 1 + .conflict-base-0/tests/task/samples.py | 86 + .conflict-base-0/tests/task/test_interval.py | 127 ++ .conflict-base-0/tests/task/test_manager.py | 81 + .conflict-base-0/tests/task/test_router.py | 175 ++ .conflict-base-0/uv.lock | 1934 +++++++++++++++++ .conflict-files | 3 + .conflict-side-0/.github/workflows/ci.yml | 94 + .../.github/workflows/release.yml | 110 + .conflict-side-0/.gitignore | 17 + .conflict-side-0/.pre-commit-config.yaml | 63 + .conflict-side-0/.vscode/settings.json | 58 + .conflict-side-0/LICENSE | 21 + .conflict-side-0/README.md | 158 ++ .conflict-side-0/docs/index.md | 158 ++ .conflict-side-0/docs/logging.md | 73 + .conflict-side-0/docs/sync.md | 81 + .conflict-side-0/docs/task.md | 85 + .conflict-side-0/examples/__init__.py | 1 + .conflict-side-0/examples/logging/__init__.py | 1 + .conflict-side-0/examples/logging/basic.log | 4 + .conflict-side-0/examples/logging/basic.py | 17 + .../examples/logging/configure_logging.py | 3 + .conflict-side-0/examples/logging/fastapi.py | 22 + .../examples/simple_fastapi_app.py | 54 + .conflict-side-0/examples/single_file_app.py | 114 + .conflict-side-0/examples/sync/__init__.py | 1 + .../examples/sync/leaderelection_anyio.py | 11 + .../examples/sync/leaderelection_task.py | 6 + .conflict-side-0/examples/sync/lock.py | 8 + .conflict-side-0/examples/sync/memory.py | 3 + .conflict-side-0/examples/sync/postgres.py | 3 + .conflict-side-0/examples/sync/redis.py | 3 + .conflict-side-0/examples/task/__init__.py | 1 + .conflict-side-0/examples/task/fastapi.py | 16 + .conflict-side-0/examples/task/faststream.py | 18 + .../examples/task/interval_manager.py | 8 + .../examples/task/interval_router.py | 8 + .../examples/task/leaderelection.py | 12 + .conflict-side-0/examples/task/lock.py | 11 + .conflict-side-0/examples/task/router.py | 15 + .conflict-side-0/grelmicro/__init__.py | 3 + .conflict-side-0/grelmicro/errors.py | 52 + .../grelmicro/logging/__init__.py | 5 + .conflict-side-0/grelmicro/logging/config.py | 43 + .conflict-side-0/grelmicro/logging/errors.py | 7 + .conflict-side-0/grelmicro/logging/loguru.py | 121 ++ .conflict-side-0/grelmicro/py.typed | 0 .conflict-side-0/grelmicro/sync/__init__.py | 6 + .conflict-side-0/grelmicro/sync/_backends.py | 30 + .conflict-side-0/grelmicro/sync/_base.py | 101 + .conflict-side-0/grelmicro/sync/_utils.py | 38 + .conflict-side-0/grelmicro/sync/abc.py | 106 + .conflict-side-0/grelmicro/sync/errors.py | 67 + .../grelmicro/sync/leaderelection.py | 386 ++++ .conflict-side-0/grelmicro/sync/lock.py | 324 +++ .conflict-side-0/grelmicro/sync/memory.py | 78 + .conflict-side-0/grelmicro/sync/postgres.py | 198 ++ .conflict-side-0/grelmicro/sync/redis.py | 146 ++ .conflict-side-0/grelmicro/task/__init__.py | 6 + .conflict-side-0/grelmicro/task/_interval.py | 92 + .conflict-side-0/grelmicro/task/_utils.py | 43 + .conflict-side-0/grelmicro/task/abc.py | 31 + .conflict-side-0/grelmicro/task/errors.py | 28 + .conflict-side-0/grelmicro/task/manager.py | 89 + .conflict-side-0/grelmicro/task/router.py | 132 ++ .conflict-side-0/mkdocs.yml | 47 + .conflict-side-0/pyproject.toml | 174 ++ .conflict-side-0/tests/__init__.py | 1 + .conflict-side-0/tests/conftest.py | 9 + .conflict-side-0/tests/logging/__init__.py | 1 + .conflict-side-0/tests/logging/test_loguru.py | 274 +++ .conflict-side-0/tests/sync/__init__.py | 1 + .conflict-side-0/tests/sync/test_backends.py | 370 ++++ .../tests/sync/test_leaderelection.py | 457 ++++ .conflict-side-0/tests/sync/test_lock.py | 506 +++++ .conflict-side-0/tests/sync/test_postgres.py | 106 + .conflict-side-0/tests/sync/test_redis.py | 67 + .conflict-side-0/tests/sync/utils.py | 23 + .conflict-side-0/tests/task/__init__.py | 1 + .conflict-side-0/tests/task/samples.py | 86 + .conflict-side-0/tests/task/test_interval.py | 127 ++ .conflict-side-0/tests/task/test_manager.py | 81 + .conflict-side-0/tests/task/test_router.py | 175 ++ .conflict-side-0/uv.lock | 1934 +++++++++++++++++ .conflict-side-1/.github/workflows/ci.yml | 94 + .../.github/workflows/release.yml | 110 + .conflict-side-1/.gitignore | 17 + .conflict-side-1/.pre-commit-config.yaml | 63 + .conflict-side-1/.vscode/settings.json | 58 + .conflict-side-1/LICENSE | 21 + .conflict-side-1/README.md | 158 ++ .conflict-side-1/docs/index.md | 158 ++ .conflict-side-1/docs/logging.md | 73 + .conflict-side-1/docs/sync.md | 81 + .conflict-side-1/docs/task.md | 85 + .conflict-side-1/examples/__init__.py | 1 + .conflict-side-1/examples/logging/__init__.py | 1 + .conflict-side-1/examples/logging/basic.log | 4 + .conflict-side-1/examples/logging/basic.py | 17 + .../examples/logging/configure_logging.py | 3 + .conflict-side-1/examples/logging/fastapi.py | 22 + .../examples/simple_fastapi_app.py | 54 + .conflict-side-1/examples/single_file_app.py | 114 + .conflict-side-1/examples/sync/__init__.py | 1 + .../examples/sync/leaderelection_anyio.py | 11 + .../examples/sync/leaderelection_task.py | 6 + .conflict-side-1/examples/sync/lock.py | 8 + .conflict-side-1/examples/sync/memory.py | 3 + .conflict-side-1/examples/sync/postgres.py | 3 + .conflict-side-1/examples/sync/redis.py | 3 + .conflict-side-1/examples/task/__init__.py | 1 + .conflict-side-1/examples/task/fastapi.py | 16 + .conflict-side-1/examples/task/faststream.py | 18 + .../examples/task/interval_manager.py | 8 + .../examples/task/interval_router.py | 8 + .../examples/task/leaderelection.py | 12 + .conflict-side-1/examples/task/lock.py | 11 + .conflict-side-1/examples/task/router.py | 15 + .conflict-side-1/grelmicro/__init__.py | 3 + .conflict-side-1/grelmicro/errors.py | 52 + .../grelmicro/logging/__init__.py | 5 + .conflict-side-1/grelmicro/logging/config.py | 43 + .conflict-side-1/grelmicro/logging/errors.py | 7 + .conflict-side-1/grelmicro/logging/loguru.py | 121 ++ .conflict-side-1/grelmicro/py.typed | 0 .conflict-side-1/grelmicro/sync/__init__.py | 6 + .conflict-side-1/grelmicro/sync/_backends.py | 30 + .conflict-side-1/grelmicro/sync/_base.py | 101 + .conflict-side-1/grelmicro/sync/_utils.py | 38 + .conflict-side-1/grelmicro/sync/abc.py | 106 + .conflict-side-1/grelmicro/sync/errors.py | 67 + .../grelmicro/sync/leaderelection.py | 386 ++++ .conflict-side-1/grelmicro/sync/lock.py | 324 +++ .conflict-side-1/grelmicro/sync/memory.py | 78 + .conflict-side-1/grelmicro/sync/postgres.py | 206 ++ .conflict-side-1/grelmicro/sync/redis.py | 146 ++ .conflict-side-1/grelmicro/task/__init__.py | 6 + .conflict-side-1/grelmicro/task/_interval.py | 92 + .conflict-side-1/grelmicro/task/_utils.py | 43 + .conflict-side-1/grelmicro/task/abc.py | 31 + .conflict-side-1/grelmicro/task/errors.py | 28 + .conflict-side-1/grelmicro/task/manager.py | 89 + .conflict-side-1/grelmicro/task/router.py | 132 ++ .conflict-side-1/mkdocs.yml | 47 + .conflict-side-1/pyproject.toml | 174 ++ .conflict-side-1/tests/__init__.py | 1 + .conflict-side-1/tests/conftest.py | 9 + .conflict-side-1/tests/logging/__init__.py | 1 + .conflict-side-1/tests/logging/test_loguru.py | 274 +++ .conflict-side-1/tests/sync/__init__.py | 1 + .conflict-side-1/tests/sync/test_backends.py | 370 ++++ .../tests/sync/test_leaderelection.py | 457 ++++ .conflict-side-1/tests/sync/test_lock.py | 506 +++++ .conflict-side-1/tests/sync/test_postgres.py | 106 + .conflict-side-1/tests/sync/test_redis.py | 67 + .conflict-side-1/tests/sync/utils.py | 23 + .conflict-side-1/tests/task/__init__.py | 1 + .conflict-side-1/tests/task/samples.py | 86 + .conflict-side-1/tests/task/test_interval.py | 127 ++ .conflict-side-1/tests/task/test_manager.py | 81 + .conflict-side-1/tests/task/test_router.py | 175 ++ .conflict-side-1/uv.lock | 1934 +++++++++++++++++ README.txt | 1 + 314 files changed, 23508 insertions(+) rename {.github => .auto-resolution/.github}/workflows/ci.yml (100%) rename {.github => .auto-resolution/.github}/workflows/release.yml (100%) rename .gitignore => .auto-resolution/.gitignore (100%) rename .pre-commit-config.yaml => .auto-resolution/.pre-commit-config.yaml (100%) rename {.vscode => .auto-resolution/.vscode}/settings.json (100%) rename LICENSE => .auto-resolution/LICENSE (100%) rename README.md => .auto-resolution/README.md (100%) rename {docs => .auto-resolution/docs}/index.md (100%) rename {docs => .auto-resolution/docs}/logging.md (100%) rename {docs => .auto-resolution/docs}/sync.md (100%) rename {docs => .auto-resolution/docs}/task.md (100%) rename {examples => .auto-resolution/examples}/__init__.py (100%) rename {examples => .auto-resolution/examples}/logging/__init__.py (100%) rename {examples => .auto-resolution/examples}/logging/basic.log (100%) rename {examples => .auto-resolution/examples}/logging/basic.py (100%) rename {examples => .auto-resolution/examples}/logging/configure_logging.py (100%) rename {examples => .auto-resolution/examples}/logging/fastapi.py (100%) rename {examples => .auto-resolution/examples}/simple_fastapi_app.py (100%) rename {examples => .auto-resolution/examples}/single_file_app.py (100%) rename {examples => .auto-resolution/examples}/sync/__init__.py (100%) rename {examples => .auto-resolution/examples}/sync/leaderelection_anyio.py (100%) rename {examples => .auto-resolution/examples}/sync/leaderelection_task.py (100%) rename {examples => .auto-resolution/examples}/sync/lock.py (100%) rename {examples => .auto-resolution/examples}/sync/memory.py (100%) rename {examples => .auto-resolution/examples}/sync/postgres.py (100%) rename {examples => .auto-resolution/examples}/sync/redis.py (100%) rename {examples => .auto-resolution/examples}/task/__init__.py (100%) rename {examples => .auto-resolution/examples}/task/fastapi.py (100%) rename {examples => .auto-resolution/examples}/task/faststream.py (100%) rename {examples => .auto-resolution/examples}/task/interval_manager.py (100%) rename {examples => .auto-resolution/examples}/task/interval_router.py (100%) rename {examples => .auto-resolution/examples}/task/leaderelection.py (100%) rename {examples => .auto-resolution/examples}/task/lock.py (100%) rename {examples => .auto-resolution/examples}/task/router.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/__init__.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/errors.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/logging/__init__.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/logging/config.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/logging/errors.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/logging/loguru.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/py.typed (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/__init__.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/_backends.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/_base.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/_utils.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/abc.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/errors.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/leaderelection.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/lock.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/memory.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/postgres.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/sync/redis.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/task/__init__.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/task/_interval.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/task/_utils.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/task/abc.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/task/errors.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/task/manager.py (100%) rename {grelmicro => .auto-resolution/grelmicro}/task/router.py (100%) rename mkdocs.yml => .auto-resolution/mkdocs.yml (100%) rename pyproject.toml => .auto-resolution/pyproject.toml (100%) rename {tests => .auto-resolution/tests}/__init__.py (100%) rename {tests => .auto-resolution/tests}/conftest.py (100%) rename {tests => .auto-resolution/tests}/logging/__init__.py (100%) rename {tests => .auto-resolution/tests}/logging/test_loguru.py (100%) rename {tests => .auto-resolution/tests}/sync/__init__.py (100%) rename {tests => .auto-resolution/tests}/sync/test_backends.py (100%) rename {tests => .auto-resolution/tests}/sync/test_leaderelection.py (100%) rename {tests => .auto-resolution/tests}/sync/test_lock.py (100%) rename {tests => .auto-resolution/tests}/sync/test_postgres.py (100%) rename {tests => .auto-resolution/tests}/sync/test_redis.py (100%) rename {tests => .auto-resolution/tests}/sync/utils.py (100%) rename {tests => .auto-resolution/tests}/task/__init__.py (100%) rename {tests => .auto-resolution/tests}/task/samples.py (100%) rename {tests => .auto-resolution/tests}/task/test_interval.py (100%) rename {tests => .auto-resolution/tests}/task/test_manager.py (100%) rename {tests => .auto-resolution/tests}/task/test_router.py (100%) rename uv.lock => .auto-resolution/uv.lock (100%) create mode 100644 .conflict-base-0/.github/workflows/ci.yml create mode 100644 .conflict-base-0/.github/workflows/release.yml create mode 100644 .conflict-base-0/.gitignore create mode 100644 .conflict-base-0/.pre-commit-config.yaml create mode 100644 .conflict-base-0/.vscode/settings.json create mode 100644 .conflict-base-0/LICENSE create mode 100644 .conflict-base-0/README.md create mode 100644 .conflict-base-0/docs/index.md create mode 100644 .conflict-base-0/docs/logging.md create mode 100644 .conflict-base-0/docs/sync.md create mode 100644 .conflict-base-0/docs/task.md create mode 100644 .conflict-base-0/examples/__init__.py create mode 100644 .conflict-base-0/examples/logging/__init__.py create mode 100644 .conflict-base-0/examples/logging/basic.log create mode 100644 .conflict-base-0/examples/logging/basic.py create mode 100644 .conflict-base-0/examples/logging/configure_logging.py create mode 100644 .conflict-base-0/examples/logging/fastapi.py create mode 100644 .conflict-base-0/examples/simple_fastapi_app.py create mode 100644 .conflict-base-0/examples/single_file_app.py create mode 100644 .conflict-base-0/examples/sync/__init__.py create mode 100644 .conflict-base-0/examples/sync/leaderelection_anyio.py create mode 100644 .conflict-base-0/examples/sync/leaderelection_task.py create mode 100644 .conflict-base-0/examples/sync/lock.py create mode 100644 .conflict-base-0/examples/sync/memory.py create mode 100644 .conflict-base-0/examples/sync/postgres.py create mode 100644 .conflict-base-0/examples/sync/redis.py create mode 100644 .conflict-base-0/examples/task/__init__.py create mode 100644 .conflict-base-0/examples/task/fastapi.py create mode 100644 .conflict-base-0/examples/task/faststream.py create mode 100644 .conflict-base-0/examples/task/interval_manager.py create mode 100644 .conflict-base-0/examples/task/interval_router.py create mode 100644 .conflict-base-0/examples/task/leaderelection.py create mode 100644 .conflict-base-0/examples/task/lock.py create mode 100644 .conflict-base-0/examples/task/router.py create mode 100644 .conflict-base-0/grelmicro/__init__.py create mode 100644 .conflict-base-0/grelmicro/errors.py create mode 100644 .conflict-base-0/grelmicro/logging/__init__.py create mode 100644 .conflict-base-0/grelmicro/logging/config.py create mode 100644 .conflict-base-0/grelmicro/logging/errors.py create mode 100644 .conflict-base-0/grelmicro/logging/loguru.py create mode 100644 .conflict-base-0/grelmicro/py.typed create mode 100644 .conflict-base-0/grelmicro/sync/__init__.py create mode 100644 .conflict-base-0/grelmicro/sync/_backends.py create mode 100644 .conflict-base-0/grelmicro/sync/_base.py create mode 100644 .conflict-base-0/grelmicro/sync/_utils.py create mode 100644 .conflict-base-0/grelmicro/sync/abc.py create mode 100644 .conflict-base-0/grelmicro/sync/errors.py create mode 100644 .conflict-base-0/grelmicro/sync/leaderelection.py create mode 100644 .conflict-base-0/grelmicro/sync/lock.py create mode 100644 .conflict-base-0/grelmicro/sync/memory.py create mode 100644 .conflict-base-0/grelmicro/sync/postgres.py create mode 100644 .conflict-base-0/grelmicro/sync/redis.py create mode 100644 .conflict-base-0/grelmicro/task/__init__.py create mode 100644 .conflict-base-0/grelmicro/task/_interval.py create mode 100644 .conflict-base-0/grelmicro/task/_utils.py create mode 100644 .conflict-base-0/grelmicro/task/abc.py create mode 100644 .conflict-base-0/grelmicro/task/errors.py create mode 100644 .conflict-base-0/grelmicro/task/manager.py create mode 100644 .conflict-base-0/grelmicro/task/router.py create mode 100644 .conflict-base-0/mkdocs.yml create mode 100644 .conflict-base-0/pyproject.toml create mode 100644 .conflict-base-0/tests/__init__.py create mode 100644 .conflict-base-0/tests/conftest.py create mode 100644 .conflict-base-0/tests/logging/__init__.py create mode 100644 .conflict-base-0/tests/logging/test_loguru.py create mode 100644 .conflict-base-0/tests/sync/__init__.py create mode 100644 .conflict-base-0/tests/sync/test_backends.py create mode 100644 .conflict-base-0/tests/sync/test_leaderelection.py create mode 100644 .conflict-base-0/tests/sync/test_lock.py create mode 100644 .conflict-base-0/tests/sync/test_postgres.py create mode 100644 .conflict-base-0/tests/sync/test_redis.py create mode 100644 .conflict-base-0/tests/sync/utils.py create mode 100644 .conflict-base-0/tests/task/__init__.py create mode 100644 .conflict-base-0/tests/task/samples.py create mode 100644 .conflict-base-0/tests/task/test_interval.py create mode 100644 .conflict-base-0/tests/task/test_manager.py create mode 100644 .conflict-base-0/tests/task/test_router.py create mode 100644 .conflict-base-0/uv.lock create mode 100644 .conflict-files create mode 100644 .conflict-side-0/.github/workflows/ci.yml create mode 100644 .conflict-side-0/.github/workflows/release.yml create mode 100644 .conflict-side-0/.gitignore create mode 100644 .conflict-side-0/.pre-commit-config.yaml create mode 100644 .conflict-side-0/.vscode/settings.json create mode 100644 .conflict-side-0/LICENSE create mode 100644 .conflict-side-0/README.md create mode 100644 .conflict-side-0/docs/index.md create mode 100644 .conflict-side-0/docs/logging.md create mode 100644 .conflict-side-0/docs/sync.md create mode 100644 .conflict-side-0/docs/task.md create mode 100644 .conflict-side-0/examples/__init__.py create mode 100644 .conflict-side-0/examples/logging/__init__.py create mode 100644 .conflict-side-0/examples/logging/basic.log create mode 100644 .conflict-side-0/examples/logging/basic.py create mode 100644 .conflict-side-0/examples/logging/configure_logging.py create mode 100644 .conflict-side-0/examples/logging/fastapi.py create mode 100644 .conflict-side-0/examples/simple_fastapi_app.py create mode 100644 .conflict-side-0/examples/single_file_app.py create mode 100644 .conflict-side-0/examples/sync/__init__.py create mode 100644 .conflict-side-0/examples/sync/leaderelection_anyio.py create mode 100644 .conflict-side-0/examples/sync/leaderelection_task.py create mode 100644 .conflict-side-0/examples/sync/lock.py create mode 100644 .conflict-side-0/examples/sync/memory.py create mode 100644 .conflict-side-0/examples/sync/postgres.py create mode 100644 .conflict-side-0/examples/sync/redis.py create mode 100644 .conflict-side-0/examples/task/__init__.py create mode 100644 .conflict-side-0/examples/task/fastapi.py create mode 100644 .conflict-side-0/examples/task/faststream.py create mode 100644 .conflict-side-0/examples/task/interval_manager.py create mode 100644 .conflict-side-0/examples/task/interval_router.py create mode 100644 .conflict-side-0/examples/task/leaderelection.py create mode 100644 .conflict-side-0/examples/task/lock.py create mode 100644 .conflict-side-0/examples/task/router.py create mode 100644 .conflict-side-0/grelmicro/__init__.py create mode 100644 .conflict-side-0/grelmicro/errors.py create mode 100644 .conflict-side-0/grelmicro/logging/__init__.py create mode 100644 .conflict-side-0/grelmicro/logging/config.py create mode 100644 .conflict-side-0/grelmicro/logging/errors.py create mode 100644 .conflict-side-0/grelmicro/logging/loguru.py create mode 100644 .conflict-side-0/grelmicro/py.typed create mode 100644 .conflict-side-0/grelmicro/sync/__init__.py create mode 100644 .conflict-side-0/grelmicro/sync/_backends.py create mode 100644 .conflict-side-0/grelmicro/sync/_base.py create mode 100644 .conflict-side-0/grelmicro/sync/_utils.py create mode 100644 .conflict-side-0/grelmicro/sync/abc.py create mode 100644 .conflict-side-0/grelmicro/sync/errors.py create mode 100644 .conflict-side-0/grelmicro/sync/leaderelection.py create mode 100644 .conflict-side-0/grelmicro/sync/lock.py create mode 100644 .conflict-side-0/grelmicro/sync/memory.py create mode 100644 .conflict-side-0/grelmicro/sync/postgres.py create mode 100644 .conflict-side-0/grelmicro/sync/redis.py create mode 100644 .conflict-side-0/grelmicro/task/__init__.py create mode 100644 .conflict-side-0/grelmicro/task/_interval.py create mode 100644 .conflict-side-0/grelmicro/task/_utils.py create mode 100644 .conflict-side-0/grelmicro/task/abc.py create mode 100644 .conflict-side-0/grelmicro/task/errors.py create mode 100644 .conflict-side-0/grelmicro/task/manager.py create mode 100644 .conflict-side-0/grelmicro/task/router.py create mode 100644 .conflict-side-0/mkdocs.yml create mode 100644 .conflict-side-0/pyproject.toml create mode 100644 .conflict-side-0/tests/__init__.py create mode 100644 .conflict-side-0/tests/conftest.py create mode 100644 .conflict-side-0/tests/logging/__init__.py create mode 100644 .conflict-side-0/tests/logging/test_loguru.py create mode 100644 .conflict-side-0/tests/sync/__init__.py create mode 100644 .conflict-side-0/tests/sync/test_backends.py create mode 100644 .conflict-side-0/tests/sync/test_leaderelection.py create mode 100644 .conflict-side-0/tests/sync/test_lock.py create mode 100644 .conflict-side-0/tests/sync/test_postgres.py create mode 100644 .conflict-side-0/tests/sync/test_redis.py create mode 100644 .conflict-side-0/tests/sync/utils.py create mode 100644 .conflict-side-0/tests/task/__init__.py create mode 100644 .conflict-side-0/tests/task/samples.py create mode 100644 .conflict-side-0/tests/task/test_interval.py create mode 100644 .conflict-side-0/tests/task/test_manager.py create mode 100644 .conflict-side-0/tests/task/test_router.py create mode 100644 .conflict-side-0/uv.lock create mode 100644 .conflict-side-1/.github/workflows/ci.yml create mode 100644 .conflict-side-1/.github/workflows/release.yml create mode 100644 .conflict-side-1/.gitignore create mode 100644 .conflict-side-1/.pre-commit-config.yaml create mode 100644 .conflict-side-1/.vscode/settings.json create mode 100644 .conflict-side-1/LICENSE create mode 100644 .conflict-side-1/README.md create mode 100644 .conflict-side-1/docs/index.md create mode 100644 .conflict-side-1/docs/logging.md create mode 100644 .conflict-side-1/docs/sync.md create mode 100644 .conflict-side-1/docs/task.md create mode 100644 .conflict-side-1/examples/__init__.py create mode 100644 .conflict-side-1/examples/logging/__init__.py create mode 100644 .conflict-side-1/examples/logging/basic.log create mode 100644 .conflict-side-1/examples/logging/basic.py create mode 100644 .conflict-side-1/examples/logging/configure_logging.py create mode 100644 .conflict-side-1/examples/logging/fastapi.py create mode 100644 .conflict-side-1/examples/simple_fastapi_app.py create mode 100644 .conflict-side-1/examples/single_file_app.py create mode 100644 .conflict-side-1/examples/sync/__init__.py create mode 100644 .conflict-side-1/examples/sync/leaderelection_anyio.py create mode 100644 .conflict-side-1/examples/sync/leaderelection_task.py create mode 100644 .conflict-side-1/examples/sync/lock.py create mode 100644 .conflict-side-1/examples/sync/memory.py create mode 100644 .conflict-side-1/examples/sync/postgres.py create mode 100644 .conflict-side-1/examples/sync/redis.py create mode 100644 .conflict-side-1/examples/task/__init__.py create mode 100644 .conflict-side-1/examples/task/fastapi.py create mode 100644 .conflict-side-1/examples/task/faststream.py create mode 100644 .conflict-side-1/examples/task/interval_manager.py create mode 100644 .conflict-side-1/examples/task/interval_router.py create mode 100644 .conflict-side-1/examples/task/leaderelection.py create mode 100644 .conflict-side-1/examples/task/lock.py create mode 100644 .conflict-side-1/examples/task/router.py create mode 100644 .conflict-side-1/grelmicro/__init__.py create mode 100644 .conflict-side-1/grelmicro/errors.py create mode 100644 .conflict-side-1/grelmicro/logging/__init__.py create mode 100644 .conflict-side-1/grelmicro/logging/config.py create mode 100644 .conflict-side-1/grelmicro/logging/errors.py create mode 100644 .conflict-side-1/grelmicro/logging/loguru.py create mode 100644 .conflict-side-1/grelmicro/py.typed create mode 100644 .conflict-side-1/grelmicro/sync/__init__.py create mode 100644 .conflict-side-1/grelmicro/sync/_backends.py create mode 100644 .conflict-side-1/grelmicro/sync/_base.py create mode 100644 .conflict-side-1/grelmicro/sync/_utils.py create mode 100644 .conflict-side-1/grelmicro/sync/abc.py create mode 100644 .conflict-side-1/grelmicro/sync/errors.py create mode 100644 .conflict-side-1/grelmicro/sync/leaderelection.py create mode 100644 .conflict-side-1/grelmicro/sync/lock.py create mode 100644 .conflict-side-1/grelmicro/sync/memory.py create mode 100644 .conflict-side-1/grelmicro/sync/postgres.py create mode 100644 .conflict-side-1/grelmicro/sync/redis.py create mode 100644 .conflict-side-1/grelmicro/task/__init__.py create mode 100644 .conflict-side-1/grelmicro/task/_interval.py create mode 100644 .conflict-side-1/grelmicro/task/_utils.py create mode 100644 .conflict-side-1/grelmicro/task/abc.py create mode 100644 .conflict-side-1/grelmicro/task/errors.py create mode 100644 .conflict-side-1/grelmicro/task/manager.py create mode 100644 .conflict-side-1/grelmicro/task/router.py create mode 100644 .conflict-side-1/mkdocs.yml create mode 100644 .conflict-side-1/pyproject.toml create mode 100644 .conflict-side-1/tests/__init__.py create mode 100644 .conflict-side-1/tests/conftest.py create mode 100644 .conflict-side-1/tests/logging/__init__.py create mode 100644 .conflict-side-1/tests/logging/test_loguru.py create mode 100644 .conflict-side-1/tests/sync/__init__.py create mode 100644 .conflict-side-1/tests/sync/test_backends.py create mode 100644 .conflict-side-1/tests/sync/test_leaderelection.py create mode 100644 .conflict-side-1/tests/sync/test_lock.py create mode 100644 .conflict-side-1/tests/sync/test_postgres.py create mode 100644 .conflict-side-1/tests/sync/test_redis.py create mode 100644 .conflict-side-1/tests/sync/utils.py create mode 100644 .conflict-side-1/tests/task/__init__.py create mode 100644 .conflict-side-1/tests/task/samples.py create mode 100644 .conflict-side-1/tests/task/test_interval.py create mode 100644 .conflict-side-1/tests/task/test_manager.py create mode 100644 .conflict-side-1/tests/task/test_router.py create mode 100644 .conflict-side-1/uv.lock create mode 100644 README.txt diff --git a/.github/workflows/ci.yml b/.auto-resolution/.github/workflows/ci.yml similarity index 100% rename from .github/workflows/ci.yml rename to .auto-resolution/.github/workflows/ci.yml diff --git a/.github/workflows/release.yml b/.auto-resolution/.github/workflows/release.yml similarity index 100% rename from .github/workflows/release.yml rename to .auto-resolution/.github/workflows/release.yml diff --git a/.gitignore b/.auto-resolution/.gitignore similarity index 100% rename from .gitignore rename to .auto-resolution/.gitignore diff --git a/.pre-commit-config.yaml b/.auto-resolution/.pre-commit-config.yaml similarity index 100% rename from .pre-commit-config.yaml rename to .auto-resolution/.pre-commit-config.yaml diff --git a/.vscode/settings.json b/.auto-resolution/.vscode/settings.json similarity index 100% rename from .vscode/settings.json rename to .auto-resolution/.vscode/settings.json diff --git a/LICENSE b/.auto-resolution/LICENSE similarity index 100% rename from LICENSE rename to .auto-resolution/LICENSE diff --git a/README.md b/.auto-resolution/README.md similarity index 100% rename from README.md rename to .auto-resolution/README.md diff --git a/docs/index.md b/.auto-resolution/docs/index.md similarity index 100% rename from docs/index.md rename to .auto-resolution/docs/index.md diff --git a/docs/logging.md b/.auto-resolution/docs/logging.md similarity index 100% rename from docs/logging.md rename to .auto-resolution/docs/logging.md diff --git a/docs/sync.md b/.auto-resolution/docs/sync.md similarity index 100% rename from docs/sync.md rename to .auto-resolution/docs/sync.md diff --git a/docs/task.md b/.auto-resolution/docs/task.md similarity index 100% rename from docs/task.md rename to .auto-resolution/docs/task.md diff --git a/examples/__init__.py b/.auto-resolution/examples/__init__.py similarity index 100% rename from examples/__init__.py rename to .auto-resolution/examples/__init__.py diff --git a/examples/logging/__init__.py b/.auto-resolution/examples/logging/__init__.py similarity index 100% rename from examples/logging/__init__.py rename to .auto-resolution/examples/logging/__init__.py diff --git a/examples/logging/basic.log b/.auto-resolution/examples/logging/basic.log similarity index 100% rename from examples/logging/basic.log rename to .auto-resolution/examples/logging/basic.log diff --git a/examples/logging/basic.py b/.auto-resolution/examples/logging/basic.py similarity index 100% rename from examples/logging/basic.py rename to .auto-resolution/examples/logging/basic.py diff --git a/examples/logging/configure_logging.py b/.auto-resolution/examples/logging/configure_logging.py similarity index 100% rename from examples/logging/configure_logging.py rename to .auto-resolution/examples/logging/configure_logging.py diff --git a/examples/logging/fastapi.py b/.auto-resolution/examples/logging/fastapi.py similarity index 100% rename from examples/logging/fastapi.py rename to .auto-resolution/examples/logging/fastapi.py diff --git a/examples/simple_fastapi_app.py b/.auto-resolution/examples/simple_fastapi_app.py similarity index 100% rename from examples/simple_fastapi_app.py rename to .auto-resolution/examples/simple_fastapi_app.py diff --git a/examples/single_file_app.py b/.auto-resolution/examples/single_file_app.py similarity index 100% rename from examples/single_file_app.py rename to .auto-resolution/examples/single_file_app.py diff --git a/examples/sync/__init__.py b/.auto-resolution/examples/sync/__init__.py similarity index 100% rename from examples/sync/__init__.py rename to .auto-resolution/examples/sync/__init__.py diff --git a/examples/sync/leaderelection_anyio.py b/.auto-resolution/examples/sync/leaderelection_anyio.py similarity index 100% rename from examples/sync/leaderelection_anyio.py rename to .auto-resolution/examples/sync/leaderelection_anyio.py diff --git a/examples/sync/leaderelection_task.py b/.auto-resolution/examples/sync/leaderelection_task.py similarity index 100% rename from examples/sync/leaderelection_task.py rename to .auto-resolution/examples/sync/leaderelection_task.py diff --git a/examples/sync/lock.py b/.auto-resolution/examples/sync/lock.py similarity index 100% rename from examples/sync/lock.py rename to .auto-resolution/examples/sync/lock.py diff --git a/examples/sync/memory.py b/.auto-resolution/examples/sync/memory.py similarity index 100% rename from examples/sync/memory.py rename to .auto-resolution/examples/sync/memory.py diff --git a/examples/sync/postgres.py b/.auto-resolution/examples/sync/postgres.py similarity index 100% rename from examples/sync/postgres.py rename to .auto-resolution/examples/sync/postgres.py diff --git a/examples/sync/redis.py b/.auto-resolution/examples/sync/redis.py similarity index 100% rename from examples/sync/redis.py rename to .auto-resolution/examples/sync/redis.py diff --git a/examples/task/__init__.py b/.auto-resolution/examples/task/__init__.py similarity index 100% rename from examples/task/__init__.py rename to .auto-resolution/examples/task/__init__.py diff --git a/examples/task/fastapi.py b/.auto-resolution/examples/task/fastapi.py similarity index 100% rename from examples/task/fastapi.py rename to .auto-resolution/examples/task/fastapi.py diff --git a/examples/task/faststream.py b/.auto-resolution/examples/task/faststream.py similarity index 100% rename from examples/task/faststream.py rename to .auto-resolution/examples/task/faststream.py diff --git a/examples/task/interval_manager.py b/.auto-resolution/examples/task/interval_manager.py similarity index 100% rename from examples/task/interval_manager.py rename to .auto-resolution/examples/task/interval_manager.py diff --git a/examples/task/interval_router.py b/.auto-resolution/examples/task/interval_router.py similarity index 100% rename from examples/task/interval_router.py rename to .auto-resolution/examples/task/interval_router.py diff --git a/examples/task/leaderelection.py b/.auto-resolution/examples/task/leaderelection.py similarity index 100% rename from examples/task/leaderelection.py rename to .auto-resolution/examples/task/leaderelection.py diff --git a/examples/task/lock.py b/.auto-resolution/examples/task/lock.py similarity index 100% rename from examples/task/lock.py rename to .auto-resolution/examples/task/lock.py diff --git a/examples/task/router.py b/.auto-resolution/examples/task/router.py similarity index 100% rename from examples/task/router.py rename to .auto-resolution/examples/task/router.py diff --git a/grelmicro/__init__.py b/.auto-resolution/grelmicro/__init__.py similarity index 100% rename from grelmicro/__init__.py rename to .auto-resolution/grelmicro/__init__.py diff --git a/grelmicro/errors.py b/.auto-resolution/grelmicro/errors.py similarity index 100% rename from grelmicro/errors.py rename to .auto-resolution/grelmicro/errors.py diff --git a/grelmicro/logging/__init__.py b/.auto-resolution/grelmicro/logging/__init__.py similarity index 100% rename from grelmicro/logging/__init__.py rename to .auto-resolution/grelmicro/logging/__init__.py diff --git a/grelmicro/logging/config.py b/.auto-resolution/grelmicro/logging/config.py similarity index 100% rename from grelmicro/logging/config.py rename to .auto-resolution/grelmicro/logging/config.py diff --git a/grelmicro/logging/errors.py b/.auto-resolution/grelmicro/logging/errors.py similarity index 100% rename from grelmicro/logging/errors.py rename to .auto-resolution/grelmicro/logging/errors.py diff --git a/grelmicro/logging/loguru.py b/.auto-resolution/grelmicro/logging/loguru.py similarity index 100% rename from grelmicro/logging/loguru.py rename to .auto-resolution/grelmicro/logging/loguru.py diff --git a/grelmicro/py.typed b/.auto-resolution/grelmicro/py.typed similarity index 100% rename from grelmicro/py.typed rename to .auto-resolution/grelmicro/py.typed diff --git a/grelmicro/sync/__init__.py b/.auto-resolution/grelmicro/sync/__init__.py similarity index 100% rename from grelmicro/sync/__init__.py rename to .auto-resolution/grelmicro/sync/__init__.py diff --git a/grelmicro/sync/_backends.py b/.auto-resolution/grelmicro/sync/_backends.py similarity index 100% rename from grelmicro/sync/_backends.py rename to .auto-resolution/grelmicro/sync/_backends.py diff --git a/grelmicro/sync/_base.py b/.auto-resolution/grelmicro/sync/_base.py similarity index 100% rename from grelmicro/sync/_base.py rename to .auto-resolution/grelmicro/sync/_base.py diff --git a/grelmicro/sync/_utils.py b/.auto-resolution/grelmicro/sync/_utils.py similarity index 100% rename from grelmicro/sync/_utils.py rename to .auto-resolution/grelmicro/sync/_utils.py diff --git a/grelmicro/sync/abc.py b/.auto-resolution/grelmicro/sync/abc.py similarity index 100% rename from grelmicro/sync/abc.py rename to .auto-resolution/grelmicro/sync/abc.py diff --git a/grelmicro/sync/errors.py b/.auto-resolution/grelmicro/sync/errors.py similarity index 100% rename from grelmicro/sync/errors.py rename to .auto-resolution/grelmicro/sync/errors.py diff --git a/grelmicro/sync/leaderelection.py b/.auto-resolution/grelmicro/sync/leaderelection.py similarity index 100% rename from grelmicro/sync/leaderelection.py rename to .auto-resolution/grelmicro/sync/leaderelection.py diff --git a/grelmicro/sync/lock.py b/.auto-resolution/grelmicro/sync/lock.py similarity index 100% rename from grelmicro/sync/lock.py rename to .auto-resolution/grelmicro/sync/lock.py diff --git a/grelmicro/sync/memory.py b/.auto-resolution/grelmicro/sync/memory.py similarity index 100% rename from grelmicro/sync/memory.py rename to .auto-resolution/grelmicro/sync/memory.py diff --git a/grelmicro/sync/postgres.py b/.auto-resolution/grelmicro/sync/postgres.py similarity index 100% rename from grelmicro/sync/postgres.py rename to .auto-resolution/grelmicro/sync/postgres.py diff --git a/grelmicro/sync/redis.py b/.auto-resolution/grelmicro/sync/redis.py similarity index 100% rename from grelmicro/sync/redis.py rename to .auto-resolution/grelmicro/sync/redis.py diff --git a/grelmicro/task/__init__.py b/.auto-resolution/grelmicro/task/__init__.py similarity index 100% rename from grelmicro/task/__init__.py rename to .auto-resolution/grelmicro/task/__init__.py diff --git a/grelmicro/task/_interval.py b/.auto-resolution/grelmicro/task/_interval.py similarity index 100% rename from grelmicro/task/_interval.py rename to .auto-resolution/grelmicro/task/_interval.py diff --git a/grelmicro/task/_utils.py b/.auto-resolution/grelmicro/task/_utils.py similarity index 100% rename from grelmicro/task/_utils.py rename to .auto-resolution/grelmicro/task/_utils.py diff --git a/grelmicro/task/abc.py b/.auto-resolution/grelmicro/task/abc.py similarity index 100% rename from grelmicro/task/abc.py rename to .auto-resolution/grelmicro/task/abc.py diff --git a/grelmicro/task/errors.py b/.auto-resolution/grelmicro/task/errors.py similarity index 100% rename from grelmicro/task/errors.py rename to .auto-resolution/grelmicro/task/errors.py diff --git a/grelmicro/task/manager.py b/.auto-resolution/grelmicro/task/manager.py similarity index 100% rename from grelmicro/task/manager.py rename to .auto-resolution/grelmicro/task/manager.py diff --git a/grelmicro/task/router.py b/.auto-resolution/grelmicro/task/router.py similarity index 100% rename from grelmicro/task/router.py rename to .auto-resolution/grelmicro/task/router.py diff --git a/mkdocs.yml b/.auto-resolution/mkdocs.yml similarity index 100% rename from mkdocs.yml rename to .auto-resolution/mkdocs.yml diff --git a/pyproject.toml b/.auto-resolution/pyproject.toml similarity index 100% rename from pyproject.toml rename to .auto-resolution/pyproject.toml diff --git a/tests/__init__.py b/.auto-resolution/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to .auto-resolution/tests/__init__.py diff --git a/tests/conftest.py b/.auto-resolution/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to .auto-resolution/tests/conftest.py diff --git a/tests/logging/__init__.py b/.auto-resolution/tests/logging/__init__.py similarity index 100% rename from tests/logging/__init__.py rename to .auto-resolution/tests/logging/__init__.py diff --git a/tests/logging/test_loguru.py b/.auto-resolution/tests/logging/test_loguru.py similarity index 100% rename from tests/logging/test_loguru.py rename to .auto-resolution/tests/logging/test_loguru.py diff --git a/tests/sync/__init__.py b/.auto-resolution/tests/sync/__init__.py similarity index 100% rename from tests/sync/__init__.py rename to .auto-resolution/tests/sync/__init__.py diff --git a/tests/sync/test_backends.py b/.auto-resolution/tests/sync/test_backends.py similarity index 100% rename from tests/sync/test_backends.py rename to .auto-resolution/tests/sync/test_backends.py diff --git a/tests/sync/test_leaderelection.py b/.auto-resolution/tests/sync/test_leaderelection.py similarity index 100% rename from tests/sync/test_leaderelection.py rename to .auto-resolution/tests/sync/test_leaderelection.py diff --git a/tests/sync/test_lock.py b/.auto-resolution/tests/sync/test_lock.py similarity index 100% rename from tests/sync/test_lock.py rename to .auto-resolution/tests/sync/test_lock.py diff --git a/tests/sync/test_postgres.py b/.auto-resolution/tests/sync/test_postgres.py similarity index 100% rename from tests/sync/test_postgres.py rename to .auto-resolution/tests/sync/test_postgres.py diff --git a/tests/sync/test_redis.py b/.auto-resolution/tests/sync/test_redis.py similarity index 100% rename from tests/sync/test_redis.py rename to .auto-resolution/tests/sync/test_redis.py diff --git a/tests/sync/utils.py b/.auto-resolution/tests/sync/utils.py similarity index 100% rename from tests/sync/utils.py rename to .auto-resolution/tests/sync/utils.py diff --git a/tests/task/__init__.py b/.auto-resolution/tests/task/__init__.py similarity index 100% rename from tests/task/__init__.py rename to .auto-resolution/tests/task/__init__.py diff --git a/tests/task/samples.py b/.auto-resolution/tests/task/samples.py similarity index 100% rename from tests/task/samples.py rename to .auto-resolution/tests/task/samples.py diff --git a/tests/task/test_interval.py b/.auto-resolution/tests/task/test_interval.py similarity index 100% rename from tests/task/test_interval.py rename to .auto-resolution/tests/task/test_interval.py diff --git a/tests/task/test_manager.py b/.auto-resolution/tests/task/test_manager.py similarity index 100% rename from tests/task/test_manager.py rename to .auto-resolution/tests/task/test_manager.py diff --git a/tests/task/test_router.py b/.auto-resolution/tests/task/test_router.py similarity index 100% rename from tests/task/test_router.py rename to .auto-resolution/tests/task/test_router.py diff --git a/uv.lock b/.auto-resolution/uv.lock similarity index 100% rename from uv.lock rename to .auto-resolution/uv.lock diff --git a/.conflict-base-0/.github/workflows/ci.yml b/.conflict-base-0/.github/workflows/ci.yml new file mode 100644 index 0000000..5fb99bb --- /dev/null +++ b/.conflict-base-0/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras + + - name: Run Mypy + run: uv run mypy . + + test: + name: Test Python ${{ matrix.python }} + runs-on: "ubuntu-latest" + strategy: + fail-fast: true + matrix: + python: ["3.11", "3.12", "3.13"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras --python ${{ matrix.python }} + + - name: Run unit tests + run: uv run pytest -x + + - name: Run integration tests + run: uv run pytest -x -m integration --cov-append + + - name: Rename coverage report + run: mv .coverage .coverage.py${{ matrix.python }} + + - name: Save coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.python }} + path: .coverage.py${{ matrix.python }} + include-hidden-files: true + + coverage-report: + name: Coverage report + runs-on: ubuntu-latest + needs: [test] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get coverage reports + uses: actions/download-artifact@v4 + with: + pattern: coverage-* + merge-multiple: true + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras + + - name: Combine coverage reports + run: | + uv run coverage combine .coverage.* + uv run coverage xml -o cov.xml + + - name: Upload coverage report to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./cov.xml diff --git a/.conflict-base-0/.github/workflows/release.yml b/.conflict-base-0/.github/workflows/release.yml new file mode 100644 index 0000000..c8d4bab --- /dev/null +++ b/.conflict-base-0/.github/workflows/release.yml @@ -0,0 +1,110 @@ +name: Release + +on: + release: + types: + - published + +jobs: + bump-version: + name: Bump version + runs-on: ubuntu-latest + steps: + + - name: Generate GitHub App Token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.GRELINFO_ID }} + private-key: ${{ secrets.GRELINFO_KEY }} + + - name: Get GitHub App User ID + id: user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + - name: Configure Git App Credentials + run: | + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}@users.noreply.github.com>' + + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Get release version + id: release-version + run: echo "release-version=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" + + - name: Get current version + id: current-version + run: echo "current-version=$(uv run hatch version)" >> "$GITHUB_OUTPUT" + + - name: Bump version if necessary + if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} + run: | + uv run hatch version $RELEASE_VERSION + uv lock + + - name: Commit and push changes + if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} + run: | + git add . + git commit -m "🚀 Release $RELEASE_VERSION" + git tag -f $RELEASE_VERSION + git push origin $RELEASE_VERSION --force + git push origin HEAD:main + + publish-docs: + runs-on: ubuntu-latest + needs: [bump-version] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Configure Git Credentials + run: | + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --group docs + + - name: Deploy docs on GitHub Pages + run: uv run mkdocs gh-deploy --force + + publish-pypi: + needs: [bump-version] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Build + run: uv build + + - name: Publish + run: uv publish -t ${{ secrets.PYPI_TOKEN }} diff --git a/.conflict-base-0/.gitignore b/.conflict-base-0/.gitignore new file mode 100644 index 0000000..0d118ab --- /dev/null +++ b/.conflict-base-0/.gitignore @@ -0,0 +1,17 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Coverage +cov.xml +.coverage + +# Mkdocs +site/ diff --git a/.conflict-base-0/.pre-commit-config.yaml b/.conflict-base-0/.pre-commit-config.yaml new file mode 100644 index 0000000..5e5a141 --- /dev/null +++ b/.conflict-base-0/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +default_language_version: + python: python3.11 + +repos: + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + - id: check-added-large-files + - id: trailing-whitespace + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.0 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + +- repo: local + hooks: + + - id: readme-to-docs + name: readme-to-docs + description: "Copy README.md to docs/index.md" + entry: cp README.md docs/index.md + language: system + pass_filenames: false + + # --- Local development hooks --- + - id: uv-lock + name: uv-lock + description: "Lock dependencies with 'uv lock'" + entry: uv lock + language: system + pass_filenames: false + + - id: mypy + name: mypy + description: "Run 'mypy' for static type checking" + entry: uv run mypy + language: system + types: [python] + require_serial: true + + - id: pytest + name: pytest + description: "Run 'pytest' for unit testing" + entry: uv run pytest --cov-fail-under=90 + language: system + pass_filenames: false + +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate + skip: [uv-lock, mypy, pytest] diff --git a/.conflict-base-0/.vscode/settings.json b/.conflict-base-0/.vscode/settings.json new file mode 100644 index 0000000..806ffc4 --- /dev/null +++ b/.conflict-base-0/.vscode/settings.json @@ -0,0 +1,58 @@ +{ + // Editor settings + "editor.rulers": [80, 100], + "files.trimTrailingWhitespace": true, + "terminal.integrated.scrollback": 10000, + + // Files exclude settings + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/__pycache__": true, + "**/.venv": true, + "**/.mypy_cache": true, + "**/.pytest_cache": true, + "**/.ruff_cache": true, + ".coverage": true + }, + + // Python settings + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.activateEnvironment": true, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": ["--no-cov", "--color=yes"], + "python.analysis.inlayHints.pytestParameters": true, + + // Python editor settings + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + + // Mypy settings + "mypy-type-checker.importStrategy": "fromEnvironment", + + // YAML settings + "yaml.schemas": { + "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + }, + "yaml.customTags": [ + "!ENV scalar", + "!ENV sequence", + "!relative scalar", + "tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg", + "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", + "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", + "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" + ], + + // Ruff settings + "ruff.configurationPreference": "filesystemFirst" +} diff --git a/.conflict-base-0/LICENSE b/.conflict-base-0/LICENSE new file mode 100644 index 0000000..18dafa2 --- /dev/null +++ b/.conflict-base-0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Loïc Gremaud + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.conflict-base-0/README.md b/.conflict-base-0/README.md new file mode 100644 index 0000000..9f3e0ff --- /dev/null +++ b/.conflict-base-0/README.md @@ -0,0 +1,158 @@ +# Grelmicro + +Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. + +It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. + +[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) +[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) + +______________________________________________________________________ + +**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) + +**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) + +______________________________________________________________________ + +## Overview + +Grelmicro provides essential features for building robust distributed systems, including: + +- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. +- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. +- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. +- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. + +These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. + +### Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. + +### Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. + +The available primitives are: + +- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **Lock**: A distributed lock that can be used to synchronize access to shared resources. + +### Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Installation + +```bash +pip install grelmicro +``` + +## Examples + +### FastAPI Integration + +- Create a file `main.py` with: + +```python +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") +``` + +## Dependencies + +Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. + +### `standard` Dependencies + +When you install Grelmicro with `pip install grelmicro[standard]` it comes with: + +- `loguru`: A Python logging library. +- `orjson`: A fast, correct JSON library for Python. + +### `redis` Dependencies + +When you install Grelmicro with `pip install grelmicro[redis]` it comes with: + +- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). + +### `postgres` Dependencies + +When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: + +- `asyncpg`: The Python `asyncio` interface for PostgreSQL. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/.conflict-base-0/docs/index.md b/.conflict-base-0/docs/index.md new file mode 100644 index 0000000..9f3e0ff --- /dev/null +++ b/.conflict-base-0/docs/index.md @@ -0,0 +1,158 @@ +# Grelmicro + +Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. + +It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. + +[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) +[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) + +______________________________________________________________________ + +**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) + +**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) + +______________________________________________________________________ + +## Overview + +Grelmicro provides essential features for building robust distributed systems, including: + +- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. +- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. +- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. +- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. + +These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. + +### Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. + +### Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. + +The available primitives are: + +- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **Lock**: A distributed lock that can be used to synchronize access to shared resources. + +### Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Installation + +```bash +pip install grelmicro +``` + +## Examples + +### FastAPI Integration + +- Create a file `main.py` with: + +```python +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") +``` + +## Dependencies + +Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. + +### `standard` Dependencies + +When you install Grelmicro with `pip install grelmicro[standard]` it comes with: + +- `loguru`: A Python logging library. +- `orjson`: A fast, correct JSON library for Python. + +### `redis` Dependencies + +When you install Grelmicro with `pip install grelmicro[redis]` it comes with: + +- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). + +### `postgres` Dependencies + +When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: + +- `asyncpg`: The Python `asyncio` interface for PostgreSQL. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/.conflict-base-0/docs/logging.md b/.conflict-base-0/docs/logging.md new file mode 100644 index 0000000..4575b03 --- /dev/null +++ b/.conflict-base-0/docs/logging.md @@ -0,0 +1,73 @@ +# Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to stdout. It supports JSON formatting and allows log level configuration via environment variables. + +## Dependencies + +For the moment the `logging` package is only working with the `loguru` Python logging library. +When `orjson` is installed, it will be used as the default JSON serializer for faster performance, otherwise, the standard `json` library will be used. + +[**Loguru**](https://loguru.readthedocs.io/en/stable/overview.html) is used as the logging library. + +For using `logging` package, please install the required dependencies: + +=== "Standard" + ```bash + pip install grelmicro[standard] + ``` + +=== "only loguru (minimum)" + ```bash + pip install loguru + ``` + +=== "loguru and orjson (manual)" + ```bash + pip install loguru orjson + ``` + + +## Configure Logging + +Just call the `configure_logging` function to set up the logging system. + +```python +{!> ../examples/logging/configure_logging.py!} +``` + +### Settings + +You can change the default settings using the following environment variables: + +- `LOG_LEVEL`: Set the desired log level (default: `INFO`). +- `LOG_FORMAT`: Choose the log format. Options are `TEXT` and `JSON`, or you can provide a custom [loguru](https://loguru.readthedocs.io/en/stable/overview.html) template (default: `TEXT`). + + +## Examples + +### Basic Usage + +Here is a quick example of how to use the logging system: + +```python +{!> ../examples/logging/basic.py!} +``` + +The console output, `stdout` will be: + +```json +{!> ../examples/logging/basic.log!} +``` + +### FastAPI Integration + +You can use the logging system with FastAPI as well: + +```python +{!> ../examples/logging/fastapi.py!} +``` + +!!! warning + It is crucial to call `configure_logging` during the lifespan of the FastAPI application. Failing to do so may result in the FastAPI CLI resetting the logging configuration. diff --git a/.conflict-base-0/docs/sync.md b/.conflict-base-0/docs/sync.md new file mode 100644 index 0000000..4c3b881 --- /dev/null +++ b/.conflict-base-0/docs/sync.md @@ -0,0 +1,81 @@ +# Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends (see more in the Backends section). + +The available primitives are: + +- **[Leader Election](#leader-election)**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **[Lock](#lock)**: A distributed lock that can be used to synchronize access to shared resources. + +The synchronization primitives can be used in combination with the `TaskManager` and `TaskRouter` to control task execution in a distributed system (see more in [Task Scheduler](task.md)). + +## Backend + +You must load a synchronization backend before using synchronization primitives. + +!!! note + Although Grelmicro use AnyIO for concurrency, the backends generally depend on `asyncio`, therefore Trio is not supported. + +You can initialize a backend like this: + +=== "Redis" + ```python + {!> ../examples/sync/redis.py!} + ``` + +=== "Postgres" + ```python + {!> ../examples/sync/postgres.py!} + ``` + +=== "Memory (For Testing Only)" + ```python + {!> ../examples/sync/memory.py!} + ``` + +!!! warning + Please make sure to use a proper way to store connection url, such as environment variables (not like the example above). + +!!! tip + Feel free to create your own backend and contribute it. In the `sync.abc` module, you can find the protocol for creating new backends. + + + +## Leader Election + +Leader election ensures that only one worker in the cluster is designated as the leader at any given time using a distributed lock. + +The leader election service is responsible for acquiring and renewing the distributed lock. It runs as an AnyIO Task that can be easily started with the [Task Manager](./task.md#task-manager). This service operates in the background, automatically renewing the lock to prevent other workers from acquiring it. The lock is released automatically when the task is cancelled or during shutdown. + +=== "Task Manager (Recommended)" + ```python + {!> ../examples/sync/leaderelection_task.py!} + ``` + +=== "AnyIO Task Group (Advanced)" + ```python + {!> ../examples/sync/leaderelection_anyio.py!} + ``` + +## Lock + +The lock is a distributed lock that can be used to synchronize access to shared resources. + +The lock supports the following features: + +- **Async**: The lock must be acquired and released asynchronously. +- **Distributed**: The lock must be distributed across multiple workers. +- **Reentrant**: The lock must allow the same token to acquire it multiple times to extend the lease. +- **Expiring**: The lock must have a timeout to auto-release after an interval to prevent deadlocks. +- **Non-blocking**: Lock operations must not block the async event loop. +- **Vendor-agnostic**: Must support multiple backends (Redis, Postgres, ConfigMap, etc.). + + +```python +{!> ../examples/sync/lock.py!} +``` + +!!! warning + The lock is designed for use within an async event loop and is not thread-safe or process-safe. diff --git a/.conflict-base-0/docs/task.md b/.conflict-base-0/docs/task.md new file mode 100644 index 0000000..b6f0e00 --- /dev/null +++ b/.conflict-base-0/docs/task.md @@ -0,0 +1,85 @@ +# Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Task Manager + +The `TaskManager` class is the main entry point to manage scheduled tasks. You need to start the task manager to run the scheduled tasks using the application lifespan. + +=== "FastAPI" + + ```python + {!> ../examples/task/fastapi.py!} + ``` + +=== "FastStream" + + ```python + + {!> ../examples/task/faststream.py!} + ``` + +## Interval Task + +To create an `IntervalTask`, use the `interval` decorator method of the `TaskManager` instance. This decorator allows tasks to run at specified intervals. + +> **Note**: The interval specifies the waiting time between task executions. Ensure that the task execution duration is considered to meet deadlines effectively. + +=== "TaskManager" + + ```python + {!> ../examples/task/interval_manager.py!} + ``` + +=== "TaskRouter" + + ```python + {!> ../examples/task/interval_router.py!} + ``` + + +## Synchronization + +The Task can be synchronized using a [Synchoronization Primitive](sync.md) to control concurrency and manage simultaneous task execution. + +=== "Lock" + + ```python + {!> ../examples/task/lock.py!} + ``` + + +=== "Leader Election" + + + ```python + {!> ../examples/task/leaderelection.py!} + ``` + +## Task Router + +For bigger applications, you can use the `TaskRouter` class to manage tasks in different modules. + + +```python +{!> ../examples/task/router.py [ln:1-10]!} +``` + +Then you can include the `TaskRouter` into the `TaskManager` or other routers using the `include_router` method. + +```python +{!> ../examples/task/router.py [ln:12-]!} +``` + +!!! tip + The `TaskRouter` follows the same philosophy as the `APIRouter` in FastAPI or the **Router** in FastStream. diff --git a/.conflict-base-0/examples/__init__.py b/.conflict-base-0/examples/__init__.py new file mode 100644 index 0000000..73b7d32 --- /dev/null +++ b/.conflict-base-0/examples/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Examples.""" diff --git a/.conflict-base-0/examples/logging/__init__.py b/.conflict-base-0/examples/logging/__init__.py new file mode 100644 index 0000000..bf04afe --- /dev/null +++ b/.conflict-base-0/examples/logging/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Logging Examples.""" diff --git a/.conflict-base-0/examples/logging/basic.log b/.conflict-base-0/examples/logging/basic.log new file mode 100644 index 0000000..33c8e37 --- /dev/null +++ b/.conflict-base-0/examples/logging/basic.log @@ -0,0 +1,4 @@ +{"time":"2024-11-25T15:56:36.066922+01:00","level":"INFO","thread":"MainThread","logger":"__main__::7","msg":"This is an info message"} +{"time":"2024-11-25T15:56:36.067063+01:00","level":"WARNING","thread":"MainThread","logger":"__main__::8","msg":"This is a warning message with context","ctx":{"user":"Alice"}} +{"time":"2024-11-25T15:56:36.067105+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::9","msg":"This is an error message with context","ctx":{"user":"Bob"}} +{"time":"2024-11-25T15:56:36.067134+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::14","msg":"This is an exception message with context","ctx":{"user":"Charlie","exception":"ValueError: This is an exception"}} diff --git a/.conflict-base-0/examples/logging/basic.py b/.conflict-base-0/examples/logging/basic.py new file mode 100644 index 0000000..889f160 --- /dev/null +++ b/.conflict-base-0/examples/logging/basic.py @@ -0,0 +1,17 @@ +from loguru import logger + +from grelmicro.logging import configure_logging + +configure_logging() + +logger.debug("This is a debug message") +logger.info("This is an info message") +logger.warning("This is a warning message with context", user="Alice") +logger.error("This is an error message with context", user="Bob") + +try: + raise ValueError("This is an exception message") +except ValueError: + logger.exception( + "This is an exception message with context", user="Charlie" + ) diff --git a/.conflict-base-0/examples/logging/configure_logging.py b/.conflict-base-0/examples/logging/configure_logging.py new file mode 100644 index 0000000..0ffacd8 --- /dev/null +++ b/.conflict-base-0/examples/logging/configure_logging.py @@ -0,0 +1,3 @@ +from grelmicro.logging import configure_logging + +configure_logging() diff --git a/.conflict-base-0/examples/logging/fastapi.py b/.conflict-base-0/examples/logging/fastapi.py new file mode 100644 index 0000000..7f318c5 --- /dev/null +++ b/.conflict-base-0/examples/logging/fastapi.py @@ -0,0 +1,22 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from loguru import logger + +from grelmicro.logging import configure_logging + + +@asynccontextmanager +def lifespan_startup(): + # Ensure logging is configured during startup + configure_logging() + yield + + +app = FastAPI() + + +@app.get("/") +def root(): + logger.info("This is an info message") + return {"Hello": "World"} diff --git a/.conflict-base-0/examples/simple_fastapi_app.py b/.conflict-base-0/examples/simple_fastapi_app.py new file mode 100644 index 0000000..ff52251 --- /dev/null +++ b/.conflict-base-0/examples/simple_fastapi_app.py @@ -0,0 +1,54 @@ +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") diff --git a/.conflict-base-0/examples/single_file_app.py b/.conflict-base-0/examples/single_file_app.py new file mode 100644 index 0000000..4f4bb87 --- /dev/null +++ b/.conflict-base-0/examples/single_file_app.py @@ -0,0 +1,114 @@ +import time +from contextlib import asynccontextmanager +from typing import Annotated + +import anyio +import typer +from fast_depends import Depends +from fastapi import FastAPI + +from grelmicro.sync.leaderelection import LeaderElection +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.task import TaskManager + +backend = MemorySyncBackend() +task = TaskManager() + + +@asynccontextmanager +async def lifespan(app): + async with backend, task: + typer.echo("App started") + yield + typer.echo("App stopped") + + +app = FastAPI(lifespan=lifespan) + +leased_lock_10sec = Lock( + name="leased_lock_10sec", + lease_duration=10, + backend=backend, +) +leased_lock_5sec = Lock( + name="leased_lock_5sec", + lease_duration=5, + backend=backend, +) + +leader_election = LeaderElection(name="simple-leader", backend=backend) + +task.add_task(leader_election) + + +@task.interval(seconds=1) +def sync_func_with_no_param(): + typer.echo("sync_with_no_param") + + +@task.interval(seconds=2) +async def async_func_with_no_param(): + typer.echo("async_with_no_param") + + +def sync_dependency(): + return "sync_dependency" + + +@task.interval(seconds=3) +def sync_func_with_sync_dependency( + sync_dependency: Annotated[str, Depends(sync_dependency)], +): + typer.echo(sync_dependency) + + +async def async_dependency(): + yield "async_with_async_dependency" + + +@task.interval(seconds=4) +async def async_func_with_async_dependency( + async_dependency: Annotated[str, Depends(async_dependency)], +): + typer.echo(async_dependency) + + +@task.interval(seconds=15, sync=leased_lock_10sec) +def sync_func_with_leased_lock_10sec(): + typer.echo("sync_func_with_leased_lock_10sec") + time.sleep(9) + + +@task.interval(seconds=15, sync=leased_lock_10sec) +async def async_func_with_leased_lock_10sec(): + typer.echo("async_func_with_leased_lock_10sec") + await anyio.sleep(9) + + +@task.interval(seconds=15, sync=leased_lock_5sec) +def sync_func_with_sync_dependency_and_leased_lock_5sec( + sync_dependency: Annotated[str, Depends(sync_dependency)], +): + typer.echo(sync_dependency) + time.sleep(4) + + +@task.interval(seconds=15, sync=leased_lock_5sec) +async def async_func_with_async_dependency_and_leased_lock_5sec( + async_dependency: Annotated[str, Depends(async_dependency)], +): + typer.echo(async_dependency) + await anyio.sleep(4) + + +@task.interval(seconds=15, sync=leader_election) +def sync_func_with_leader_election(): + typer.echo("sync_func_with_leader_election") + time.sleep(30) + + +@task.interval(seconds=15, sync=leader_election) +async def async_func_with_leader_election(): + typer.echo("async_func_with_leader_election") + await anyio.sleep(30) diff --git a/.conflict-base-0/examples/sync/__init__.py b/.conflict-base-0/examples/sync/__init__.py new file mode 100644 index 0000000..acd409a --- /dev/null +++ b/.conflict-base-0/examples/sync/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Synchronization Primitives Examples.""" diff --git a/.conflict-base-0/examples/sync/leaderelection_anyio.py b/.conflict-base-0/examples/sync/leaderelection_anyio.py new file mode 100644 index 0000000..784f188 --- /dev/null +++ b/.conflict-base-0/examples/sync/leaderelection_anyio.py @@ -0,0 +1,11 @@ +from anyio import create_task_group, sleep_forever + +from grelmicro.sync.leaderelection import LeaderElection + +leader = LeaderElection("cluster_group") + + +async def main(): + async with create_task_group() as tg: + await tg.start(leader) + await sleep_forever() diff --git a/.conflict-base-0/examples/sync/leaderelection_task.py b/.conflict-base-0/examples/sync/leaderelection_task.py new file mode 100644 index 0000000..58fa926 --- /dev/null +++ b/.conflict-base-0/examples/sync/leaderelection_task.py @@ -0,0 +1,6 @@ +from grelmicro.sync import LeaderElection +from grelmicro.task import TaskManager + +leader = LeaderElection("cluster_group") +task = TaskManager() +task.add_task(leader) diff --git a/.conflict-base-0/examples/sync/lock.py b/.conflict-base-0/examples/sync/lock.py new file mode 100644 index 0000000..7f38fe6 --- /dev/null +++ b/.conflict-base-0/examples/sync/lock.py @@ -0,0 +1,8 @@ +from grelmicro.sync import Lock + +lock = Lock("resource_name") + + +async def main(): + async with lock: + print("Protected resource accessed") diff --git a/.conflict-base-0/examples/sync/memory.py b/.conflict-base-0/examples/sync/memory.py new file mode 100644 index 0000000..7eefea9 --- /dev/null +++ b/.conflict-base-0/examples/sync/memory.py @@ -0,0 +1,3 @@ +from grelmicro.sync.memory import MemorySyncBackend + +backend = MemorySyncBackend() diff --git a/.conflict-base-0/examples/sync/postgres.py b/.conflict-base-0/examples/sync/postgres.py new file mode 100644 index 0000000..ea8b8c3 --- /dev/null +++ b/.conflict-base-0/examples/sync/postgres.py @@ -0,0 +1,3 @@ +from grelmicro.sync.postgres import PostgresSyncBackend + +backend = PostgresSyncBackend("postgresql://user:password@localhost:5432/db") diff --git a/.conflict-base-0/examples/sync/redis.py b/.conflict-base-0/examples/sync/redis.py new file mode 100644 index 0000000..0625f5d --- /dev/null +++ b/.conflict-base-0/examples/sync/redis.py @@ -0,0 +1,3 @@ +from grelmicro.sync.redis import RedisSyncBackend + +backend = RedisSyncBackend("redis://localhost:6379/0") diff --git a/.conflict-base-0/examples/task/__init__.py b/.conflict-base-0/examples/task/__init__.py new file mode 100644 index 0000000..20f7752 --- /dev/null +++ b/.conflict-base-0/examples/task/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Task Scheduler Examples.""" diff --git a/.conflict-base-0/examples/task/fastapi.py b/.conflict-base-0/examples/task/fastapi.py new file mode 100644 index 0000000..16aaa8e --- /dev/null +++ b/.conflict-base-0/examples/task/fastapi.py @@ -0,0 +1,16 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from grelmicro.task import TaskManager + +task = TaskManager() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + async with task: + yield + + +app = FastAPI(lifespan=lifespan) diff --git a/.conflict-base-0/examples/task/faststream.py b/.conflict-base-0/examples/task/faststream.py new file mode 100644 index 0000000..688c8d9 --- /dev/null +++ b/.conflict-base-0/examples/task/faststream.py @@ -0,0 +1,18 @@ +from contextlib import asynccontextmanager + +from faststream import ContextRepo, FastStream +from faststream.redis import RedisBroker + +from grelmicro.task import TaskManager + +task = TaskManager() + + +@asynccontextmanager +async def lifespan(context: ContextRepo): + async with task: + yield + + +broker = RedisBroker() +app = FastStream(broker, lifespan=lifespan) diff --git a/.conflict-base-0/examples/task/interval_manager.py b/.conflict-base-0/examples/task/interval_manager.py new file mode 100644 index 0000000..91beb2e --- /dev/null +++ b/.conflict-base-0/examples/task/interval_manager.py @@ -0,0 +1,8 @@ +from grelmicro.task import TaskManager + +task = TaskManager() + + +@task.interval(seconds=5) +async def my_task(): + print("Hello, World!") diff --git a/.conflict-base-0/examples/task/interval_router.py b/.conflict-base-0/examples/task/interval_router.py new file mode 100644 index 0000000..f114ad7 --- /dev/null +++ b/.conflict-base-0/examples/task/interval_router.py @@ -0,0 +1,8 @@ +from grelmicro.task import TaskRouter + +task = TaskRouter() + + +@task.interval(seconds=5) +async def my_task(): + print("Hello, World!") diff --git a/.conflict-base-0/examples/task/leaderelection.py b/.conflict-base-0/examples/task/leaderelection.py new file mode 100644 index 0000000..ad12773 --- /dev/null +++ b/.conflict-base-0/examples/task/leaderelection.py @@ -0,0 +1,12 @@ +from grelmicro.sync import LeaderElection +from grelmicro.task import TaskManager + +leader = LeaderElection("my_task") +task = TaskManager() +task.add_task(leader) + + +@task.interval(seconds=5, sync=leader) +async def my_task(): + async with leader: + print("Hello, World!") diff --git a/.conflict-base-0/examples/task/lock.py b/.conflict-base-0/examples/task/lock.py new file mode 100644 index 0000000..cdbf795 --- /dev/null +++ b/.conflict-base-0/examples/task/lock.py @@ -0,0 +1,11 @@ +from grelmicro.sync import Lock +from grelmicro.task import TaskManager + +lock = Lock("my_task") +task = TaskManager() + + +@task.interval(seconds=5, sync=lock) +async def my_task(): + async with lock: + print("Hello, World!") diff --git a/.conflict-base-0/examples/task/router.py b/.conflict-base-0/examples/task/router.py new file mode 100644 index 0000000..2b166aa --- /dev/null +++ b/.conflict-base-0/examples/task/router.py @@ -0,0 +1,15 @@ +from grelmicro.task import TaskRouter + + +router = TaskRouter() + + +@router.interval(seconds=5) +async def my_task(): + print("Hello, World!") + + +from grelmicro.task.manager import TaskManager + +task = TaskManager() +task.include_router(router) diff --git a/.conflict-base-0/grelmicro/__init__.py b/.conflict-base-0/grelmicro/__init__.py new file mode 100644 index 0000000..7cc6d82 --- /dev/null +++ b/.conflict-base-0/grelmicro/__init__.py @@ -0,0 +1,3 @@ +"""Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python.""" # noqa: E501 + +__version__ = "0.2.2" diff --git a/.conflict-base-0/grelmicro/errors.py b/.conflict-base-0/grelmicro/errors.py new file mode 100644 index 0000000..141f82e --- /dev/null +++ b/.conflict-base-0/grelmicro/errors.py @@ -0,0 +1,52 @@ +"""Grelmicro Errors.""" + +from typing import assert_never + +from pydantic import ValidationError + + +class GrelmicroError(Exception): + """Base Grelmicro error.""" + + +class OutOfContextError(GrelmicroError, RuntimeError): + """Outside Context Error. + + Raised when a method is called outside of the context manager. + """ + + def __init__(self, cls: object, method_name: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not call {cls.__class__.__name__}.{method_name} outside of the context manager" + ) + + +class DependencyNotFoundError(GrelmicroError, ImportError): + """Dependency Not Found Error.""" + + def __init__(self, *, module: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not import module {module}, try running 'pip install {module}'" + ) + + +class SettingsValidationError(GrelmicroError, ValueError): + """Settings Validation Error.""" + + def __init__(self, error: ValidationError | str) -> None: + """Initialize the error.""" + if isinstance(error, str): + details = error + elif isinstance(error, ValidationError): + details = "\n".join( + f"- {data['loc'][0]}: {data['msg']} [input={data['input']}]" + for data in error.errors() + ) + else: + assert_never(error) + + super().__init__( + f"Could not validate environment variables settings:\n{details}" + ) diff --git a/.conflict-base-0/grelmicro/logging/__init__.py b/.conflict-base-0/grelmicro/logging/__init__.py new file mode 100644 index 0000000..60d3d45 --- /dev/null +++ b/.conflict-base-0/grelmicro/logging/__init__.py @@ -0,0 +1,5 @@ +"""Grelmicro Logging.""" + +from grelmicro.logging.loguru import configure_logging + +__all__ = ["configure_logging"] diff --git a/.conflict-base-0/grelmicro/logging/config.py b/.conflict-base-0/grelmicro/logging/config.py new file mode 100644 index 0000000..a6301c1 --- /dev/null +++ b/.conflict-base-0/grelmicro/logging/config.py @@ -0,0 +1,43 @@ +"""Logging Configuration.""" + +from enum import StrEnum +from typing import Self + +from pydantic import Field +from pydantic_settings import BaseSettings + + +class _CaseInsensitiveEnum(StrEnum): + @classmethod + def _missing_(cls, value: object) -> Self | None: + value = str(value).lower() + for member in cls: + if member.lower() == value: + return member + return None + + +class LoggingLevelType(_CaseInsensitiveEnum): + """Logging Level Enum.""" + + DEBUG = "DEBUG" + INFO = "INFO" + WARNING = "WARNING" + ERROR = "ERROR" + CRITICAL = "CRITICAL" + + +class LoggingFormatType(_CaseInsensitiveEnum): + """Logging Format Enum.""" + + JSON = "JSON" + TEXT = "TEXT" + + +class LoggingSettings(BaseSettings): + """Logging Settings.""" + + LOG_LEVEL: LoggingLevelType = LoggingLevelType.INFO + LOG_FORMAT: LoggingFormatType | str = Field( + LoggingFormatType.JSON, union_mode="left_to_right" + ) diff --git a/.conflict-base-0/grelmicro/logging/errors.py b/.conflict-base-0/grelmicro/logging/errors.py new file mode 100644 index 0000000..097006f --- /dev/null +++ b/.conflict-base-0/grelmicro/logging/errors.py @@ -0,0 +1,7 @@ +"""Grelmicro Logging Errors.""" + +from grelmicro.errors import SettingsValidationError + + +class LoggingSettingsValidationError(SettingsValidationError): + """Logging Settings Validation Error.""" diff --git a/.conflict-base-0/grelmicro/logging/loguru.py b/.conflict-base-0/grelmicro/logging/loguru.py new file mode 100644 index 0000000..a94202c --- /dev/null +++ b/.conflict-base-0/grelmicro/logging/loguru.py @@ -0,0 +1,121 @@ +"""Loguru Logging.""" + +import json +import sys +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, NotRequired + +from pydantic import ValidationError +from typing_extensions import TypedDict + +from grelmicro.errors import DependencyNotFoundError +from grelmicro.logging.config import LoggingFormatType, LoggingSettings +from grelmicro.logging.errors import LoggingSettingsValidationError + +if TYPE_CHECKING: + from loguru import FormatFunction, Record + +try: + import loguru +except ImportError: # pragma: no cover + loguru = None # type: ignore[assignment] + +try: + import orjson + + def _json_dumps(obj: Mapping[str, Any]) -> str: + return orjson.dumps(obj).decode("utf-8") +except ImportError: # pragma: no cover + import json + + _json_dumps = json.dumps + + +JSON_FORMAT = "{extra[serialized]}" +TEXT_FORMAT = ( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + "{name}:{function}:{line} - {message}" +) + + +class JSONRecordDict(TypedDict): + """JSON log record representation. + + The time use a ISO 8601 string. + """ + + time: str + level: str + msg: str + logger: str | None + thread: str + ctx: NotRequired[dict[Any, Any]] + + +def json_patcher(record: "Record") -> None: + """Patch the serialized log record with `JSONRecordDict` representation.""" + json_record = JSONRecordDict( + time=record["time"].isoformat(), + level=record["level"].name, + thread=record["thread"].name, + logger=f'{record["name"]}:{record["function"]}:{record["line"]}', + msg=record["message"], + ) + + ctx = {k: v for k, v in record["extra"].items() if k != "serialized"} + exception = record["exception"] + + if exception and exception.type: + ctx["exception"] = f"{exception.type.__name__}: {exception.value!s}" + + if ctx: + json_record["ctx"] = ctx + + record["extra"]["serialized"] = _json_dumps(json_record) + + +def json_formatter(record: "Record") -> str: + """Format log record with `JSONRecordDict` representation. + + This function does not return the formatted record directly but provides the format to use when + writing to the sink. + """ + json_patcher(record) + return JSON_FORMAT + "\n" + + +def configure_logging() -> None: + """Configure logging with loguru. + + Simple twelve-factor app logging configuration that logs to stdout. + + The following environment variables are used: + - LOG_LEVEL: The log level to use (default: INFO). + - LOG_FORMAT: JSON | TEXT or any loguru template to format logged message (default: JSON). + + Raises: + MissingDependencyError: If the loguru module is not installed. + LoggingSettingsError: If the LOG_FORMAT or LOG_LEVEL environment variable is invalid + """ + if not loguru: + raise DependencyNotFoundError(module="loguru") + + try: + settings = LoggingSettings() + except ValidationError as error: + raise LoggingSettingsValidationError(error) from None + + logger = loguru.logger + log_format: str | FormatFunction = settings.LOG_FORMAT + + if log_format is LoggingFormatType.JSON: + log_format = json_formatter + elif log_format is LoggingFormatType.TEXT: + log_format = TEXT_FORMAT + + logger.remove() + logger.add( + sys.stdout, + level=settings.LOG_LEVEL, + format=log_format, + ) diff --git a/.conflict-base-0/grelmicro/py.typed b/.conflict-base-0/grelmicro/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/.conflict-base-0/grelmicro/sync/__init__.py b/.conflict-base-0/grelmicro/sync/__init__.py new file mode 100644 index 0000000..128d56c --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/__init__.py @@ -0,0 +1,6 @@ +"""Grelmicro Synchronization Primitives.""" + +from grelmicro.sync.leaderelection import LeaderElection +from grelmicro.sync.lock import Lock + +__all__ = ["LeaderElection", "Lock"] diff --git a/.conflict-base-0/grelmicro/sync/_backends.py b/.conflict-base-0/grelmicro/sync/_backends.py new file mode 100644 index 0000000..66f4b9f --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/_backends.py @@ -0,0 +1,30 @@ +"""Grelmicro Backend Registry. + +Contains loaded backends of each type to be used as default. + +Note: + For now, only lock backends are supported, but other backends may be added in the future. +""" + +from typing import Literal, NotRequired, TypedDict + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import BackendNotLoadedError + + +class LoadedBackendsDict(TypedDict): + """Loaded backends type.""" + + lock: NotRequired[SyncBackend] + + +loaded_backends: LoadedBackendsDict = {} + + +def get_sync_backend() -> SyncBackend: + """Get the lock backend.""" + backend: Literal["lock"] = "lock" + try: + return loaded_backends[backend] + except KeyError: + raise BackendNotLoadedError(backend) from None diff --git a/.conflict-base-0/grelmicro/sync/_base.py b/.conflict-base-0/grelmicro/sync/_base.py new file mode 100644 index 0000000..a0e6fb0 --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/_base.py @@ -0,0 +1,101 @@ +"""Grelmicro Lock API.""" + +from types import TracebackType +from typing import Annotated, Protocol, Self +from uuid import UUID + +from pydantic import BaseModel, ConfigDict +from typing_extensions import Doc + +from grelmicro.sync.abc import Synchronization + + +class BaseLockConfig(BaseModel): + """Base Lock Config.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + name: Annotated[ + str, + Doc(""" + The name of the resource to lock. + """), + ] + worker: Annotated[ + str | UUID, + Doc(""" + The worker identity. + + By default, use a UUIDv1. + """), + ] + + +class BaseLock(Synchronization, Protocol): + """Base Lock Protocol.""" + + async def __aenter__(self) -> Self: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + """ + ... + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + ... + + @property + def config(self) -> BaseLockConfig: + """Return the config.""" + ... + + async def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + ... + + async def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + WouldBlock: If the lock cannot be acquired without blocking. + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + ... + + async def release(self) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + ... + + async def locked(self) -> bool: + """Check if the lock is currently held.""" + ... + + async def owned(self) -> bool: + """Check if the lock is currently held by the current token.""" + ... diff --git a/.conflict-base-0/grelmicro/sync/_utils.py b/.conflict-base-0/grelmicro/sync/_utils.py new file mode 100644 index 0000000..2ad5dda --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/_utils.py @@ -0,0 +1,38 @@ +from threading import get_ident +from uuid import NAMESPACE_DNS, UUID, uuid3 + +from anyio import get_current_task + + +def generate_worker_namespace(worker: str) -> UUID: + """Generate a worker UUIDv3 namespace. + + Generate a worker UUID using UUIDv3 with the DNS namespace. + """ + return uuid3(namespace=NAMESPACE_DNS, name=worker) + + +def generate_task_token(worker: UUID | str) -> str: + """Generate a task UUID. + + The worker namespace is generated using `generate_worker_uuid` if the worker is a string. + Generate a task UUID using UUIDv3 with the worker namespace and the async task ID. + """ + worker = ( + generate_worker_namespace(worker) if isinstance(worker, str) else worker + ) + task = str(get_current_task().id) + return str(uuid3(namespace=worker, name=task)) + + +def generate_thread_token(worker: UUID | str) -> str: + """Generate a thread UUID. + + The worker namespace is generated using `generate_worker_uuid` if the worker is a string. + Generate a thread UUID using UUIDv3 with the worker namespace and the current thread ID. + """ + worker = ( + generate_worker_namespace(worker) if isinstance(worker, str) else worker + ) + thread = str(get_ident()) + return str(uuid3(namespace=worker, name=thread)) diff --git a/.conflict-base-0/grelmicro/sync/abc.py b/.conflict-base-0/grelmicro/sync/abc.py new file mode 100644 index 0000000..507477c --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/abc.py @@ -0,0 +1,106 @@ +"""Grelmicro Synchronization Abstract Base Classes and Protocols.""" + +from types import TracebackType +from typing import Protocol, Self, runtime_checkable + +from pydantic import PositiveFloat + + +class SyncBackend(Protocol): + """Synchronization Backend Protocol. + + This is the low level API for the distributed lock backend that is platform agnostic. + """ + + async def __aenter__(self) -> Self: + """Open the lock backend.""" + ... + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Close the lock backend.""" + ... + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock. + + Args: + name: The name of the lock. + token: The token to acquire the lock. + duration: The duration in seconds to hold the lock. + + Returns: + True if the lock is acquired, False if the lock is already acquired by another token. + + Raises: + Exception: Any exception can be raised if the lock cannot be acquired. + """ + ... + + async def release(self, *, name: str, token: str) -> bool: + """Release a lock. + + Args: + name: The name of the lock. + token: The token to release the lock. + + Returns: + True if the lock was released, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock cannot be released. + """ + ... + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired. + + Args: + name: The name of the lock. + + Returns: + True if the lock is acquired, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock status cannot be checked. + """ + ... + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned. + + Args: + name: The name of the lock. + token: The token to check. + + Returns: + True if the lock is owned by the token, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock status cannot be checked. + """ + ... + + +@runtime_checkable +class Synchronization(Protocol): + """Synchronization Primitive Protocol.""" + + async def __aenter__(self) -> Self: + """Enter the synchronization primitive.""" + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the synchronization primitive.""" + ... + + +Seconds = PositiveFloat diff --git a/.conflict-base-0/grelmicro/sync/errors.py b/.conflict-base-0/grelmicro/sync/errors.py new file mode 100644 index 0000000..6384e36 --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/errors.py @@ -0,0 +1,67 @@ +"""Grelmicro Synchronization Primitive Errors.""" + +from grelmicro.errors import SettingsValidationError + + +class SyncError(Exception): + """Synchronization Primitive Error. + + This the base class for all lock errors. + """ + + +class SyncBackendError(SyncError): + """Synchronization Backend Error.""" + + +class BackendNotLoadedError(SyncBackendError): + """Backend Not Loaded Error.""" + + def __init__(self, backend_name: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not load backend {backend_name}, try initializing one first" + ) + + +class LockAcquireError(SyncBackendError): + """Acquire Lock Error. + + This error is raised when an error on backend side occurs during lock acquisition. + """ + + def __init__(self, *, name: str, token: str) -> None: + """Initialize the error.""" + super().__init__(f"Failed to acquire lock: name={name}, token={token}") + + +class LockReleaseError(SyncBackendError): + """Lock Release Error. + + This error is raised when an error on backend side occurs during lock release. + """ + + def __init__( + self, *, name: str, token: str, reason: str | None = None + ) -> None: + """Initialize the error.""" + super().__init__( + f"Failed to release lock: name={name}, token={token}" + + (f", reason={reason}" if reason else ""), + ) + + +class LockNotOwnedError(LockReleaseError): + """Lock Not Owned Error during Release. + + This error is raised when an attempt is made to release a lock that is not owned, respectively + the token is different or the lock is already expired. + """ + + def __init__(self, *, name: str, token: str) -> None: + """Initialize the error.""" + super().__init__(name=name, token=token, reason="lock not owned") + + +class SyncSettingsValidationError(SyncError, SettingsValidationError): + """Synchronization Settings Validation Error.""" diff --git a/.conflict-base-0/grelmicro/sync/leaderelection.py b/.conflict-base-0/grelmicro/sync/leaderelection.py new file mode 100644 index 0000000..62ce539 --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/leaderelection.py @@ -0,0 +1,386 @@ +"""Leader Election.""" + +from logging import getLogger +from time import monotonic +from types import TracebackType +from typing import TYPE_CHECKING, Annotated, Self +from uuid import UUID, uuid1 + +from anyio import ( + TASK_STATUS_IGNORED, + CancelScope, + Condition, + fail_after, + get_cancelled_exc_class, + move_on_after, + sleep, +) +from anyio.abc import TaskStatus +from pydantic import BaseModel, model_validator +from typing_extensions import Doc + +from grelmicro.sync._backends import get_sync_backend +from grelmicro.sync.abc import Seconds, SyncBackend, Synchronization +from grelmicro.task.abc import Task + +if TYPE_CHECKING: + from contextlib import AsyncExitStack + + from anyio.abc import TaskGroup + +logger = getLogger("grelmicro.leader_election") + + +class LeaderElectionConfig(BaseModel): + """Leader Election Config. + + Leader election based on a leased reentrant distributed lock. + """ + + name: Annotated[ + str, + Doc( + """ + The leader election lock name. + """, + ), + ] + worker: Annotated[ + str | UUID, + Doc( + """ + The worker identity used as lock token. + """, + ), + ] + lease_duration: Annotated[ + Seconds, + Doc( + """ + The lease duration in seconds. + """, + ), + ] = 15 + renew_deadline: Annotated[ + Seconds, + Doc( + """ + The renew deadline in seconds. + """, + ), + ] = 10 + retry_interval: Annotated[ + Seconds, + Doc( + """ + The retry interval in seconds. + """, + ), + ] = 2 + backend_timeout: Annotated[ + Seconds, + Doc( + """ + The backend timeout in seconds. + """, + ), + ] = 5 + error_interval: Annotated[ + Seconds, + Doc( + """ + The error interval in seconds. + """, + ), + ] = 30 + + @model_validator(mode="after") + def _validate(self) -> Self: + if self.renew_deadline >= self.lease_duration: + msg = "Renew deadline must be shorter than lease duration" + raise ValueError(msg) + if self.retry_interval >= self.renew_deadline: + msg = "Retry interval must be shorter than renew deadline" + raise ValueError(msg) + if self.backend_timeout >= self.renew_deadline: + msg = "Backend timeout must be shorter than renew deadline" + raise ValueError(msg) + return self + + +class LeaderElection(Synchronization, Task): + """Leader Election. + + The leader election is a synchronization primitive with the worker as scope. + It runs as a task to acquire or renew the distributed lock. + """ + + def __init__( + self, + name: Annotated[ + str, + Doc( + """ + The name of the resource representing the leader election. + + It will be used as the lock name so make sure it is unique on the distributed lock + backend. + """, + ), + ], + *, + backend: Annotated[ + SyncBackend | None, + Doc( + """ + The distributed lock backend used to acquire and release the lock. + + By default, it will use the lock backend registry to get the default lock backend. + """, + ), + ] = None, + worker: Annotated[ + str | UUID | None, + Doc( + """ + The worker identity. + + By default, use a UUIDv1 will be generated. + """, + ), + ] = None, + lease_duration: Annotated[ + Seconds, + Doc( + """ + The duration in seconds after the lock will be released if not renewed. + + If the worker becomes unavailable, the lock can only be acquired by an other worker + after it' has expired. + """, + ), + ] = 15, + renew_deadline: Annotated[ + Seconds, + Doc( + """ + The duration in seconds that the leader worker will try to acquire the lock before + giving up. + + Must be shorter than the lease duration. In case of multiple failures, the leader + worker will loose the lead to prevent split-brain scenarios and ensure that only one + worker is the leader at any time. + """, + ), + ] = 10, + retry_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between attempts to acquire or renew the lock. + + Must be shorter than the renew deadline. A shorter schedule enables faster leader + elections but may increase load on the distributed lock backend, while a longer + schedule reduces load but can delay new leader elections. + """, + ), + ] = 2, + backend_timeout: Annotated[ + Seconds, + Doc( + """ + The duration in seconds for waiting on backend for acquiring and releasing the lock. + + This value determines how long the system will wait before giving up the current + operation. + """, + ), + ] = 5, + error_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between logging error messages. + + If shorter than the retry interval, it will log every error. It is used to prevent + flooding the logs when the lock backend is unavailable. + """, + ), + ] = 30, + ) -> None: + """Initialize the leader election.""" + self.config = LeaderElectionConfig( + name=name, + worker=worker or uuid1(), + lease_duration=lease_duration, + renew_deadline=renew_deadline, + retry_interval=retry_interval, + backend_timeout=backend_timeout, + error_interval=error_interval, + ) + self.backend = backend or get_sync_backend() + + self._service_running = False + self._state_change_condition: Condition = Condition() + self._is_leader: bool = False + self._state_updated_at: float = monotonic() + self._error_logged_at: float | None = None + self._task_group: TaskGroup | None = None + self._exit_stack: AsyncExitStack | None = None + + async def __aenter__(self) -> Self: + """Wait for the leader with the context manager.""" + await self.wait_for_leader() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the context manager.""" + + @property + def name(self) -> str: + """Return the task name.""" + return self.config.name + + def is_running(self) -> bool: + """Check if the leader election task is running.""" + return self._service_running + + def is_leader(self) -> bool: + """Check if the current worker is the leader. + + To avoid a split-brain scenario, the leader considers itself as no longer leader if the + renew deadline is reached. + + Returns: + True if the current worker is the leader, False otherwise. + + """ + if not self._is_leader: + return False + return not self._is_renew_deadline_reached() + + async def wait_for_leader(self) -> None: + """Wait until the current worker is the leader.""" + while not self.is_leader(): + async with self._state_change_condition: + await self._state_change_condition.wait() + + async def wait_lose_leader(self) -> None: + """Wait until the current worker is no longer the leader.""" + while self.is_leader(): + with move_on_after(self._seconds_before_expiration_deadline()): + async with self._state_change_condition: + await self._state_change_condition.wait() + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run polling loop service to acquire or renew the distributed lock.""" + task_status.started() + if self._service_running: + logger.warning("Leader Election already running: %s", self.name) + return + self._service_running = True + logger.info("Leader Election started: %s", self.name) + try: + while True: + await self._try_acquire_or_renew() + await sleep(self.config.retry_interval) + except get_cancelled_exc_class(): + logger.info("Leader Election stopped: %s", self.name) + raise + except BaseException: + logger.exception("Leader Election crashed: %s", self.name) + raise + finally: + self._service_running = False + with CancelScope(shield=True): + await self._release() + + async def _update_state( + self, *, is_leader: bool, raison_if_no_more_leader: str + ) -> None: + """Update the state of the leader election.""" + self._state_updated_at = monotonic() + if is_leader is self._is_leader: + return # No change + + self._is_leader = is_leader + + if is_leader: + logger.info("Leader Election acquired leadership: %s", self.name) + else: + logger.warning( + "Leader Election lost leadership: %s (%s)", + self.name, + raison_if_no_more_leader, + ) + + async with self._state_change_condition: + self._state_change_condition.notify_all() + + async def _try_acquire_or_renew(self) -> None: + """Try to acquire leadership.""" + try: + with fail_after(self.config.backend_timeout): + is_leader = await self.backend.acquire( + name=self.name, + token=str(self.config.worker), + duration=self.config.lease_duration, + ) + except Exception: + if self._check_error_interval(): + logger.exception( + "Leader Election failed to acquire lock: %s", self.name + ) + if self._is_renew_deadline_reached(): + await self._update_state( + is_leader=False, + raison_if_no_more_leader="renew deadline reached", + ) + else: + await self._update_state( + is_leader=is_leader, + raison_if_no_more_leader="lock not acquired", + ) + + def _seconds_before_expiration_deadline(self) -> float: + return max( + self._state_updated_at + self.config.lease_duration - monotonic(), 0 + ) + + def _check_error_interval(self) -> bool: + """Check if the cooldown interval allows to log the error.""" + is_logging_allowed = ( + not self._error_logged_at + or (monotonic() - self._error_logged_at) + > self.config.error_interval + ) + self._error_logged_at = monotonic() + return is_logging_allowed + + def _is_renew_deadline_reached(self) -> bool: + return ( + monotonic() - self._state_updated_at + ) >= self.config.renew_deadline + + async def _release(self) -> None: + try: + with fail_after(self.config.backend_timeout): + if not ( + await self.backend.release( + name=self.config.name, token=str(self.config.worker) + ) + ): + logger.info( + "Leader Election lock already released: %s", self.name + ) + except Exception: + logger.exception( + "Leader Election failed to release lock: %s", self.name + ) diff --git a/.conflict-base-0/grelmicro/sync/lock.py b/.conflict-base-0/grelmicro/sync/lock.py new file mode 100644 index 0000000..c87d08f --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/lock.py @@ -0,0 +1,324 @@ +"""Grelmicro Lock.""" + +from time import sleep as thread_sleep +from types import TracebackType +from typing import Annotated, Self +from uuid import UUID, uuid1 + +from anyio import WouldBlock, from_thread, sleep +from typing_extensions import Doc + +from grelmicro.sync._backends import get_sync_backend +from grelmicro.sync._base import BaseLock, BaseLockConfig +from grelmicro.sync._utils import generate_task_token, generate_thread_token +from grelmicro.sync.abc import Seconds, SyncBackend +from grelmicro.sync.errors import ( + LockAcquireError, + LockNotOwnedError, + LockReleaseError, + SyncBackendError, +) + + +class LockConfig(BaseLockConfig, frozen=True, extra="forbid"): + """Lock Config.""" + + lease_duration: Annotated[ + Seconds, + Doc( + """ + The lease duration in seconds for the lock. + """, + ), + ] + retry_interval: Annotated[ + Seconds, + Doc( + """ + The interval in seconds between attempts to acquire the lock. + """, + ), + ] + + +class Lock(BaseLock): + """Lock. + + This lock is a distributed lock that is used to acquire a resource across multiple workers. The + lock is acquired asynchronously and can be extended multiple times manually. The lock is + automatically released after a duration if not extended. + """ + + def __init__( + self, + name: Annotated[ + str, + Doc( + """ + The name of the resource to lock. + + It will be used as the lock name so make sure it is unique on the lock backend. + """, + ), + ], + *, + backend: Annotated[ + SyncBackend | None, + Doc(""" + The distributed lock backend used to acquire and release the lock. + + By default, it will use the lock backend registry to get the default lock backend. + """), + ] = None, + worker: Annotated[ + str | UUID | None, + Doc( + """ + The worker identity. + + By default, use a UUIDv1 will be generated. + """, + ), + ] = None, + lease_duration: Annotated[ + Seconds, + Doc( + """ + The duration in seconds for the lock to be held by default. + """, + ), + ] = 60, + retry_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between attempts to acquire the lock. + + Should be greater or equal than 0.1 to prevent flooding the lock backend. + """, + ), + ] = 0.1, + ) -> None: + """Initialize the lock.""" + self._config: LockConfig = LockConfig( + name=name, + worker=worker or uuid1(), + lease_duration=lease_duration, + retry_interval=retry_interval, + ) + self.backend = backend or get_sync_backend() + self._from_thread: ThreadLockAdapter | None = None + + async def __aenter__(self) -> Self: + """Acquire the lock with the async context manager.""" + await self.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock with the async context manager. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + await self.release() + + @property + def config(self) -> LockConfig: + """Return the lock config.""" + return self._config + + @property + def from_thread(self) -> "ThreadLockAdapter": + """Return the lock adapter for worker thread.""" + if self._from_thread is None: + self._from_thread = ThreadLockAdapter(lock=self) + return self._from_thread + + async def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + while not await self.do_acquire(token=token): # noqa: ASYNC110 // Polling is intentional + await sleep(self._config.retry_interval) + + async def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + WouldBlock: If the lock cannot be acquired without blocking. + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + if not await self.do_acquire(token=token): + msg = f"Lock not acquired: name={self._config.name}, token={token}" + raise WouldBlock(msg) + + async def release(self) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + if not await self.do_release(token): + raise LockNotOwnedError(name=self._config.name, token=token) + + async def locked(self) -> bool: + """Check if the lock is acquired. + + Raise: + SyncBackendError: If the lock cannot be checked due to an error on the backend. + """ + try: + return await self.backend.locked(name=self._config.name) + except Exception as exc: + msg = "Failed to check if the lock is acquired" + raise SyncBackendError(msg) from exc + + async def owned(self) -> bool: + """Check if the lock is owned by the current token. + + Raise: + SyncBackendError: If the lock cannot be checked due to an error on the backend. + """ + return await self.do_owned(generate_task_token(self._config.worker)) + + async def do_acquire(self, token: str) -> bool: + """Acquire the lock. + + This method should not be called directly. Use `acquire` instead. + + Returns: + bool: True if the lock was acquired, False if the lock was not acquired. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + """ + try: + return await self.backend.acquire( + name=self._config.name, + token=token, + duration=self._config.lease_duration, + ) + except Exception as exc: + raise LockAcquireError(name=self._config.name, token=token) from exc + + async def do_release(self, token: str) -> bool: + """Release the lock. + + This method should not be called directly. Use `release` instead. + + Returns: + bool: True if the lock was released, False otherwise. + + Raises: + LockReleaseError: Cannot release the lock due to backend error. + """ + try: + return await self.backend.release( + name=self._config.name, token=token + ) + except Exception as exc: + raise LockReleaseError(name=self._config.name, token=token) from exc + + async def do_owned(self, token: str) -> bool: + """Check if the lock is owned by the current token. + + This method should not be called directly. Use `owned` instead. + + Returns: + bool: True if the lock is owned by the current token, False otherwise. + + Raises: + SyncBackendError: Cannot check if the lock is owned due to backend error. + """ + try: + return await self.backend.owned(name=self._config.name, token=token) + except Exception as exc: + msg = "Failed to check if the lock is owned" + raise SyncBackendError(msg) from exc + + +class ThreadLockAdapter: + """Lock Adapter for Worker Thread.""" + + def __init__(self, lock: Lock) -> None: + """Initialize the lock adapter.""" + self._lock = lock + + def __enter__(self) -> Self: + """Acquire the lock with the context manager.""" + self.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock with the context manager.""" + self.release() + + def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: Cannot acquire the lock due to backend error. + + """ + token = generate_thread_token(self._lock.config.worker) + retry_interval = self._lock.config.retry_interval + while not from_thread.run(self._lock.do_acquire, token): + thread_sleep(retry_interval) + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + LockAcquireError: Cannot acquire the lock due to backend error. + WouldBlock: If the lock cannot be acquired without blocking. + + """ + token = generate_thread_token(self._lock.config.worker) + if not from_thread.run(self._lock.do_acquire, token): + msg = f"Lock not acquired: name={self._lock.config.name}, token={token}" + raise WouldBlock(msg) + + def release(self) -> None: + """Release the lock. + + Raises: + ReleaseSyncBackendError: Cannot release the lock due to backend error. + LockNotOwnedError: If the lock is not currently held. + + """ + token = generate_thread_token(self._lock.config.worker) + if not from_thread.run(self._lock.do_release, token): + raise LockNotOwnedError(name=self._lock.config.name, token=token) + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + return from_thread.run(self._lock.locked) + + def owned(self) -> bool: + """Return True if the lock is currently held by the current worker thread.""" + return from_thread.run( + self._lock.do_owned, generate_thread_token(self._lock.config.worker) + ) diff --git a/.conflict-base-0/grelmicro/sync/memory.py b/.conflict-base-0/grelmicro/sync/memory.py new file mode 100644 index 0000000..9746c59 --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/memory.py @@ -0,0 +1,78 @@ +"""Memory Synchronization Backend.""" + +from time import monotonic +from types import TracebackType +from typing import Annotated, Self + +from typing_extensions import Doc + +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend + + +class MemorySyncBackend(SyncBackend): + """Memory Synchronization Backend. + + This is not a backend with a real distributed lock. It is a local lock that can be used for + testing purposes or for locking operations that are executed in the same AnyIO event loop. + """ + + def __init__( + self, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + ) -> None: + """Initialize the lock backend.""" + self._locks: dict[str, tuple[str | None, float]] = {} + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Enter the lock backend.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Exit the lock backend.""" + self._locks.clear() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + if ( + current_token is None + or current_token == token + or expire_at < monotonic() + ): + self._locks[name] = (token, monotonic() + duration) + return True + return False + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + if current_token == token and expire_at >= monotonic(): + del self._locks[name] + return True + if current_token and expire_at < monotonic(): + del self._locks[name] + return False + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + return current_token is not None and expire_at >= monotonic() + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + return current_token == token and expire_at >= monotonic() diff --git a/.conflict-base-0/grelmicro/sync/postgres.py b/.conflict-base-0/grelmicro/sync/postgres.py new file mode 100644 index 0000000..8614e89 --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/postgres.py @@ -0,0 +1,204 @@ +"""PostgreSQL Synchronization Backend.""" + +from types import TracebackType +from typing import Annotated, Self + +from asyncpg import Pool, create_pool +from pydantic import PostgresDsn +from pydantic_core import MultiHostUrl, ValidationError +from pydantic_settings import BaseSettings +from typing_extensions import Doc + +from grelmicro.errors import OutOfContextError +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import SyncSettingsValidationError + + +class _PostgresSettings(BaseSettings): + POSTGRES_HOST: str | None = None + POSTGRES_PORT: int = 5432 + POSTGRES_DB: str | None = None + POSTGRES_USER: str | None = None + POSTGRES_PASSWORD: str | None = None + POSTGRES_URL: PostgresDsn | None = None + + +def _get_postgres_url() -> str: + """Get the PostgreSQL URL from the environment variables. + + Raises: + SyncSettingsValidationError: If the URL or all of the host, database, user, and password + """ + try: + settings = _PostgresSettings() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + parts_fields = [ + settings.POSTGRES_HOST, + settings.POSTGRES_DB, + settings.POSTGRES_USER, + settings.POSTGRES_PASSWORD, + ] + + if settings.POSTGRES_URL and not any(parts_fields): + return settings.POSTGRES_URL.unicode_string() + + if all(parts_fields) and not settings.POSTGRES_URL: + return MultiHostUrl.build( + scheme="postgresql", + username=settings.POSTGRES_USER, + password=settings.POSTGRES_PASSWORD, + host=settings.POSTGRES_HOST, + port=settings.POSTGRES_PORT, + path=f"/{settings.POSTGRES_DB}", + ).unicode_string() + + msg = ( + "Either POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and " + "POSTGRES_PASSWORD must be set" + ) + raise SyncSettingsValidationError(msg) + + +class PostgresSyncBackend(SyncBackend): + """PostgreSQL Synchronization Backend.""" + + _SQL_CREATE_TABLE_IF_NOT_EXISTS = """ + CREATE TABLE IF NOT EXISTS {table_name} ( + name TEXT PRIMARY KEY, + token TEXT NOT NULL, + expire_at TIMESTAMP NOT NULL + ); + """ + + _SQL_ACQUIRE_OR_EXTEND = """ + INSERT INTO {table_name} (name, token, expire_at) + VALUES ($1, $2, NOW() + make_interval(secs => $3)) + ON CONFLICT (name) DO UPDATE + SET token = EXCLUDED.token, expire_at = EXCLUDED.expire_at + WHERE {table_name}.token = EXCLUDED.token OR {table_name}.expire_at < NOW() + RETURNING 1; + """ + + _SQL_RELEASE = """ + DELETE FROM {table_name} + WHERE name = $1 AND token = $2 AND expire_at >= NOW() + RETURNING 1; + """ + + _SQL_RELEASE_ALL_EXPIRED = """ + DELETE FROM {table_name} + WHERE expire_at < NOW(); + """ + + _SQL_LOCKED = """ + SELECT 1 FROM {table_name} + WHERE name = $1 AND expire_at >= NOW(); + """ + + _SQL_OWNED = """ + SELECT 1 FROM {table_name} + WHERE name = $1 AND token = $2 AND expire_at >= NOW(); + """ + + def __init__( + self, + url: Annotated[ + PostgresDsn | str | None, + Doc(""" + The Postgres database URL. + + If not provided, the URL will be taken from the environment variables POSTGRES_URL + or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD. + """), + ] = None, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + table_name: Annotated[ + str, Doc("The table name to store the locks.") + ] = "locks", + ) -> None: + """Initialize the lock backend.""" + if not table_name.isidentifier(): + msg = f"Table name '{table_name}' is not a valid identifier" + raise ValueError(msg) + + self._url = url or _get_postgres_url() + self._table_name = table_name + self._acquire_sql = self._SQL_ACQUIRE_OR_EXTEND.format( + table_name=table_name + ) + self._release_sql = self._SQL_RELEASE.format(table_name=table_name) + self._pool: Pool | None = None + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Enter the lock backend.""" + self._pool = await create_pool(str(self._url)) + await self._pool.execute( + self._SQL_CREATE_TABLE_IF_NOT_EXISTS.format( + table_name=self._table_name + ), + ) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Exit the lock backend.""" + if self._pool: + await self._pool.execute( + self._SQL_RELEASE_ALL_EXPIRED.format( + table_name=self._table_name + ), + ) + await self._pool.close() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire a lock.""" + if not self._pool: + raise OutOfContextError(self, "acquire") + + return bool( + await self._pool.fetchval(self._acquire_sql, name, token, duration) + ) + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + if not self._pool: + raise OutOfContextError(self, "release") + return bool(await self._pool.fetchval(self._release_sql, name, token)) + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + if not self._pool: + raise OutOfContextError(self, "locked") + return bool( + await self._pool.fetchval( + self._SQL_LOCKED.format(table_name=self._table_name), + name, + ), + ) + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + if not self._pool: + raise OutOfContextError(self, "owned") + return bool( + await self._pool.fetchval( + self._SQL_OWNED.format(table_name=self._table_name), + name, + token, + ), + ) diff --git a/.conflict-base-0/grelmicro/sync/redis.py b/.conflict-base-0/grelmicro/sync/redis.py new file mode 100644 index 0000000..73090c8 --- /dev/null +++ b/.conflict-base-0/grelmicro/sync/redis.py @@ -0,0 +1,146 @@ +"""Redis Synchronization Backend.""" + +from types import TracebackType +from typing import Annotated, Self + +from pydantic import RedisDsn, ValidationError +from pydantic_core import Url +from pydantic_settings import BaseSettings +from redis.asyncio.client import Redis +from typing_extensions import Doc + +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import SyncSettingsValidationError + + +class _RedisSettings(BaseSettings): + """Redis settings from the environment variables.""" + + REDIS_HOST: str | None = None + REDIS_PORT: int = 6379 + REDIS_DB: int = 0 + REDIS_PASSWORD: str | None = None + REDIS_URL: RedisDsn | None = None + + +def _get_redis_url() -> str: + """Get the Redis URL from the environment variables. + + Raises: + SyncSettingsValidationError: If the URL or host is not set. + """ + try: + settings = _RedisSettings() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + if settings.REDIS_URL and not settings.REDIS_HOST: + return settings.REDIS_URL.unicode_string() + + if settings.REDIS_HOST and not settings.REDIS_URL: + return Url.build( + scheme="redis", + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + path=str(settings.REDIS_DB), + password=settings.REDIS_PASSWORD, + ).unicode_string() + + msg = "Either REDIS_URL or REDIS_HOST must be set" + raise SyncSettingsValidationError(msg) + + +class RedisSyncBackend(SyncBackend): + """Redis Synchronization Backend.""" + + _LUA_ACQUIRE_OR_EXTEND = """ + local token = redis.call('get', KEYS[1]) + if not token then + redis.call('set', KEYS[1], ARGV[1], 'px', ARGV[2]) + return 1 + end + if token == ARGV[1] then + redis.call('pexpire', KEYS[1], ARGV[2]) + return 1 + end + return 0 + """ + _LUA_RELEASE = """ + local token = redis.call('get', KEYS[1]) + if not token or token ~= ARGV[1] then + return 0 + end + redis.call('del', KEYS[1]) + return 1 + """ + + def __init__( + self, + url: Annotated[ + RedisDsn | str | None, + Doc(""" + The Redis URL. + + If not provided, the URL will be taken from the environment variables REDIS_URL + or REDIS_HOST, REDIS_PORT, REDIS_DB, and REDIS_PASSWORD. + """), + ] = None, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + ) -> None: + """Initialize the lock backend.""" + self._url = url or _get_redis_url() + self._redis: Redis = Redis.from_url(str(self._url)) + self._lua_release = self._redis.register_script(self._LUA_RELEASE) + self._lua_acquire = self._redis.register_script( + self._LUA_ACQUIRE_OR_EXTEND + ) + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Open the lock backend.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Close the lock backend.""" + await self._redis.aclose() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock.""" + return bool( + await self._lua_acquire( + keys=[name], + args=[token, int(duration * 1000)], + client=self._redis, + ) + ) + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + return bool( + await self._lua_release( + keys=[name], args=[token], client=self._redis + ) + ) + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + return bool(await self._redis.get(name)) + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + return bool( + (await self._redis.get(name)) == token.encode() + ) # redis returns bytes diff --git a/.conflict-base-0/grelmicro/task/__init__.py b/.conflict-base-0/grelmicro/task/__init__.py new file mode 100644 index 0000000..374bf08 --- /dev/null +++ b/.conflict-base-0/grelmicro/task/__init__.py @@ -0,0 +1,6 @@ +"""Grelmicro Task Scheduler.""" + +from grelmicro.task.manager import TaskManager +from grelmicro.task.router import TaskRouter + +__all__ = ["TaskManager", "TaskRouter"] diff --git a/.conflict-base-0/grelmicro/task/_interval.py b/.conflict-base-0/grelmicro/task/_interval.py new file mode 100644 index 0000000..f66c2f2 --- /dev/null +++ b/.conflict-base-0/grelmicro/task/_interval.py @@ -0,0 +1,92 @@ +"""Interval Task.""" + +from collections.abc import Awaitable, Callable +from contextlib import nullcontext +from functools import partial +from inspect import iscoroutinefunction +from logging import getLogger +from typing import Any + +from anyio import TASK_STATUS_IGNORED, sleep, to_thread +from anyio.abc import TaskStatus +from fast_depends import inject + +from grelmicro.sync.abc import Synchronization +from grelmicro.task._utils import validate_and_generate_reference +from grelmicro.task.abc import Task + +logger = getLogger("grelmicro.task") + + +class IntervalTask(Task): + """Interval Task. + + Use the `TaskManager.interval()` or `SchedulerRouter.interval()` decorator instead + of creating IntervalTask objects directly. + """ + + def __init__( + self, + *, + function: Callable[..., Any], + name: str | None = None, + interval: float, + sync: Synchronization | None = None, + ) -> None: + """Initialize the IntervalTask. + + Raises: + FunctionNotSupportedError: If the function is not supported. + ValueError: If internal is less than or equal to 0. + """ + if interval <= 0: + msg = "Interval must be greater than 0" + raise ValueError(msg) + + alt_name = validate_and_generate_reference(function) + self._name = name or alt_name + self._interval = interval + self._async_function = self._prepare_async_function(function) + self._sync = sync if sync else nullcontext() + + @property + def name(self) -> str: + """Return the lock name.""" + return self._name + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run the repeated task loop.""" + logger.info( + "Task started (interval: %ss): %s", self._interval, self.name + ) + task_status.started() + try: + while True: + try: + async with self._sync: + try: + await self._async_function() + except Exception: + logger.exception( + "Task execution error: %s", self.name + ) + except Exception: + logger.exception( + "Task synchronization error: %s", self.name + ) + await sleep(self._interval) + finally: + logger.info("Task stopped: %s", self.name) + + def _prepare_async_function( + self, function: Callable[..., Any] + ) -> Callable[..., Awaitable[Any]]: + """Prepare the function with lock and ensure async function.""" + function = inject(function) + return ( + function + if iscoroutinefunction(function) + else partial(to_thread.run_sync, function) + ) diff --git a/.conflict-base-0/grelmicro/task/_utils.py b/.conflict-base-0/grelmicro/task/_utils.py new file mode 100644 index 0000000..7cfec3f --- /dev/null +++ b/.conflict-base-0/grelmicro/task/_utils.py @@ -0,0 +1,43 @@ +"""Task Utilities.""" + +from collections.abc import Callable +from functools import partial +from inspect import ismethod +from typing import Any + +from grelmicro.task.errors import FunctionTypeError + + +def validate_and_generate_reference(function: Callable[..., Any]) -> str: + """Generate a task name from the given function. + + This implementation is inspirated by the APScheduler project under MIT License. + Original source: https://github.com/agronholm/apscheduler/blob/master/src/apscheduler/_marshalling.py + + Raises: + FunctionNotSupportedError: If function is not supported. + + """ + if isinstance(function, partial): + ref = "partial()" + raise FunctionTypeError(ref) + + if ismethod(function): + ref = "method" + raise FunctionTypeError(ref) + + if not hasattr(function, "__module__") or not hasattr( + function, "__qualname__" + ): + ref = "callable without __module__ or __qualname__ attribute" + raise FunctionTypeError(ref) + + if "" in function.__qualname__: + ref = "lambda" + raise FunctionTypeError(ref) + + if "" in function.__qualname__: + ref = "nested function" + raise FunctionTypeError(ref) + + return f"{function.__module__}:{function.__qualname__}" diff --git a/.conflict-base-0/grelmicro/task/abc.py b/.conflict-base-0/grelmicro/task/abc.py new file mode 100644 index 0000000..d4e7cf3 --- /dev/null +++ b/.conflict-base-0/grelmicro/task/abc.py @@ -0,0 +1,31 @@ +"""Grelmicro Task Synchronization Abstract Base Classes and Protocols.""" + +from typing import Protocol + +from anyio import TASK_STATUS_IGNORED +from anyio.abc import TaskStatus +from typing_extensions import runtime_checkable + + +@runtime_checkable +class Task(Protocol): + """Task Protocol. + + A task that runs in background in the async event loop. + """ + + @property + def name(self) -> str: + """Name to uniquely identify the task.""" + ... + + async def __call__( + self, + *, + task_status: TaskStatus[None] = TASK_STATUS_IGNORED, + ) -> None: + """Run the task. + + This is the entry point of the task to be run in the async event loop. + """ + ... diff --git a/.conflict-base-0/grelmicro/task/errors.py b/.conflict-base-0/grelmicro/task/errors.py new file mode 100644 index 0000000..a788f61 --- /dev/null +++ b/.conflict-base-0/grelmicro/task/errors.py @@ -0,0 +1,28 @@ +"""Grelmicro Task Scheduler Errors.""" + +from grelmicro.errors import GrelmicroError + + +class TaskError(GrelmicroError): + """Base Grelmicro Task error.""" + + +class FunctionTypeError(TaskError, TypeError): + """Function Type Error.""" + + def __init__(self, reference: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not use function {reference}, " + "try declaring 'def' or 'async def' directly in the module" + ) + + +class TaskAddOperationError(TaskError, RuntimeError): + """Task Add Operation Error.""" + + def __init__(self) -> None: + """Initialize the error.""" + super().__init__( + "Could not add the task, try calling 'add_task' and 'include_router' before starting" + ) diff --git a/.conflict-base-0/grelmicro/task/manager.py b/.conflict-base-0/grelmicro/task/manager.py new file mode 100644 index 0000000..5432145 --- /dev/null +++ b/.conflict-base-0/grelmicro/task/manager.py @@ -0,0 +1,89 @@ +"""Grelmicro Task Manager.""" + +from contextlib import AsyncExitStack +from logging import getLogger +from types import TracebackType +from typing import TYPE_CHECKING, Annotated, Self + +from anyio import create_task_group +from typing_extensions import Doc + +from grelmicro.errors import OutOfContextError +from grelmicro.task.abc import Task +from grelmicro.task.errors import TaskAddOperationError +from grelmicro.task.router import TaskRouter + +if TYPE_CHECKING: + from anyio.abc import TaskGroup + +logger = getLogger("grelmicro.task") + + +class TaskManager(TaskRouter): + """Task Manager. + + `TaskManager` class, the main entrypoint to manage scheduled tasks. + """ + + def __init__( + self, + *, + auto_start: Annotated[ + bool, + Doc( + """ + Automatically start all tasks. + """, + ), + ] = True, + tasks: Annotated[ + list[Task] | None, + Doc( + """ + A list of tasks to be started. + """, + ), + ] = None, + ) -> None: + """Initialize the task manager.""" + TaskRouter.__init__(self, tasks=tasks) + + self._auto_start = auto_start + self._task_group: TaskGroup | None = None + + async def __aenter__(self) -> Self: + """Enter the context manager.""" + self._exit_stack = AsyncExitStack() + await self._exit_stack.__aenter__() + self._task_group = await self._exit_stack.enter_async_context( + create_task_group(), + ) + if self._auto_start: + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the context manager.""" + if not self._task_group or not self._exit_stack: + raise OutOfContextError(self, "__aexit__") + self._task_group.cancel_scope.cancel() + return await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb) + + async def start(self) -> None: + """Start all tasks manually.""" + if not self._task_group: + raise OutOfContextError(self, "start") + + if self._started: + raise TaskAddOperationError + + self.do_mark_as_started() + + for task in self.tasks: + await self._task_group.start(task.__call__) + logger.debug("%s scheduled tasks started", len(self._tasks)) diff --git a/.conflict-base-0/grelmicro/task/router.py b/.conflict-base-0/grelmicro/task/router.py new file mode 100644 index 0000000..16b240d --- /dev/null +++ b/.conflict-base-0/grelmicro/task/router.py @@ -0,0 +1,132 @@ +"""Grelmicro Task Router.""" + +from collections.abc import Awaitable, Callable +from typing import Annotated, Any + +from typing_extensions import Doc + +from grelmicro.sync.abc import Synchronization +from grelmicro.task.abc import Task +from grelmicro.task.errors import TaskAddOperationError + + +class TaskRouter: + """Task Router. + + `TaskRouter` class, used to group task schedules, for example to structure an app in + multiple files. It would then included in the `TaskManager`, or in another + `TaskRouter`. + """ + + def __init__( + self, + *, + tasks: Annotated[ + list[Task] | None, + Doc( + """ + A list of schedules or scheduled tasks to be scheduled. + """, + ), + ] = None, + ) -> None: + """Initialize the task router.""" + self._started = False + self._tasks: list[Task] = tasks or [] + self._routers: list[TaskRouter] = [] + + @property + def tasks(self) -> list[Task]: + """List of scheduled tasks.""" + return self._tasks + [ + task for router in self._routers for task in router.tasks + ] + + def add_task(self, task: Task) -> None: + """Add a task to the scheduler.""" + if self._started: + raise TaskAddOperationError + + self._tasks.append(task) + + def interval( + self, + *, + seconds: Annotated[ + float, + Doc( + """ + The duration in seconds between each task run. + + Accuracy is not guaranteed and may vary with system load. Consider the + execution time of the task when setting the interval. + """, + ), + ], + name: Annotated[ + str | None, + Doc( + """ + The name of the task. + + If None, a name will be generated automatically from the function. + """, + ), + ] = None, + sync: Annotated[ + Synchronization | None, + Doc( + """ + The synchronization primitive to use for the task. + + You can use a `LeasedLock` or a `LeaderElection`, for example. If None, + no synchronization is used and the task will run on all workers. + """, + ), + ] = None, + ) -> Callable[ + [Callable[..., Any | Awaitable[Any]]], + Callable[..., Any | Awaitable[Any]], + ]: + """Decorate function to add it to the task scheduler. + + Raises: + TaskNameGenerationError: If the task name generation fails. + """ + from grelmicro.task._interval import IntervalTask + + def decorator( + function: Callable[[], None | Awaitable[None]], + ) -> Callable[[], None | Awaitable[None]]: + self.add_task( + IntervalTask( + name=name, + function=function, + interval=seconds, + sync=sync, + ), + ) + return function + + return decorator + + def include_router(self, router: "TaskRouter") -> None: + """Include another router in this router.""" + if self._started: + raise TaskAddOperationError + + self._routers.append(router) + + def started(self) -> bool: + """Check if the task manager has started.""" + return self._started + + def do_mark_as_started(self) -> None: + """Mark the task manager as started. + + Do not call this method directly. It is called by the task manager when the task + manager is started. + """ + self._started = True + for router in self._routers: + router.do_mark_as_started() diff --git a/.conflict-base-0/mkdocs.yml b/.conflict-base-0/mkdocs.yml new file mode 100644 index 0000000..0b08e9f --- /dev/null +++ b/.conflict-base-0/mkdocs.yml @@ -0,0 +1,47 @@ +site_name: Grelmicro +site_description: Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. +site_url: https://grelmicro.grel.info +theme: + name: material + palette: + primary: green + accent: light green + font: + text: 'Roboto' + code: 'Roboto Mono' + features: + - content.tabs.link + - content.code.copy + - content.code.select + - content.tooltips + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.progress + - navigation.top + - navigation.tracking + +repo_name: grelinfo/grelmicro +repo_url: https://github.com/grelinfo/grelmicro + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + +nav: +- Grelmicro: index.md +- User Guide: + - logging.md + - sync.md + - task.md + +markdown_extensions: + - admonition + - mdx_include: + base_path: docs + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.inlinehilite + - pymdownx.tabbed: + alternate_style: true diff --git a/.conflict-base-0/pyproject.toml b/.conflict-base-0/pyproject.toml new file mode 100644 index 0000000..9bcca87 --- /dev/null +++ b/.conflict-base-0/pyproject.toml @@ -0,0 +1,174 @@ +[project] +name = "grelmicro" +description = "Grelmicro is a lightweight framework/toolkit for building async microservices in Python" +license = "MIT" +authors = [{ name = "Loïc Gremaud", email = "grelinfo@gmail.com"}] +readme = "README.md" + +classifiers = [ + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", + "Typing :: Typed", + "Development Status :: 1 - Planning", + "Environment :: Web Environment", + "Framework :: AsyncIO", + "Framework :: FastAPI", + "Framework :: Pydantic", + "Framework :: Pydantic :: 2", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dynamic = ["version"] + +requires-python = ">=3.11" + +dependencies = [ + "anyio>=4.0.0", + "pydantic>=2.5.0", + "fast-depends>=2.0.0", + "pydantic-settings>=2.5.0", +] + +[project.urls] + +Repository = "https://github.com/grelinfo/grelmicro.git" +Issues = "https://github.com/grelinfo/grelmicro/issues" + +[project.optional-dependencies] +standard = [ + "loguru>=0.7.2", + "orjson>=3.10.11", +] +postgres = [ + "asyncpg>=0.30.0", +] +redis = [ + "redis>=5.0.0", +] + +[dependency-groups] +dev = [ + "pytest-cov>=6.0.0", + "pytest>=8.0.0", + "mypy>=1.12.0", + "ruff>=0.7.4", + "testcontainers[postgres,redis]>=4.8.2", + "pytest-timeout>=2.3.1", + "pytest-mock>=3.14.0", + "pytest-randomly>=3.16.0", + "pre-commit>=4.0.1", + "fastapi>=0.115.5", + "fastapi-cli>=0.0.5", + "mdx-include>=1.4.2", + "faststream>=0.5.30", + "hatch>=1.13.0", +] +docs = [ + "mkdocs-material>=9.5.44", + "pygments>=2.18.0", + "pymdown-extensions>=10.12", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build] +skip-excluded-dirs = true +exclude = ["/tests", "/docs", "/examples"] + +[tool.hatch.version] +path = "grelmicro/__init__.py" + +[tool.ruff] +target-version = "py311" +line-length = 80 + +[tool.ruff.lint] +select = ["ALL"] +ignore = ["COM812", "ISC001"] # Ignore rules conflicting with the formatter. + +[tool.ruff.lint.extend-per-file-ignores] +"examples/*" = [ + "ARG001", + "ANN001", + "ANN201", + "D103", + "D100", + "INP001", + "T201", +] +"examples/logging/basic.py" = ["EM101", "TRY"] +"examples/task/router.py" = ["I001", "E402"] +"tests/*" = [ + "S101", + "SLF001" +] + +[tool.ruff.lint.pycodestyle] +max-line-length = 100 # reports only line that exceed 100 characters. + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.pylint] +max-args = 10 + +[tool.mypy] +scripts_are_modules = true +plugins = [ + "pydantic.mypy" +] +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = true +check_untyped_defs = true +no_implicit_reexport = true +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = ["asyncpg", "testcontainers.*"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = [ + "examples.*" +] +disallow_untyped_defs = false + + +[tool.pytest.ini_options] +addopts = """ + --cov=grelmicro + --cov-report term:skip-covered + --cov-report xml:cov.xml + --strict-config + --strict-markers + -m "not integration" +""" +markers = """ + integration: mark a test as an integration test (disabled by default). +""" + +testpaths = "tests" + +[tool.coverage.report] +sort = "-Cover" +exclude_also = [ + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "assert_never\\(.*\\)", +] diff --git a/.conflict-base-0/tests/__init__.py b/.conflict-base-0/tests/__init__.py new file mode 100644 index 0000000..adc28b2 --- /dev/null +++ b/.conflict-base-0/tests/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Tests.""" diff --git a/.conflict-base-0/tests/conftest.py b/.conflict-base-0/tests/conftest.py new file mode 100644 index 0000000..916c148 --- /dev/null +++ b/.conflict-base-0/tests/conftest.py @@ -0,0 +1,9 @@ +"""Grelmicro Test Config.""" + +import pytest + + +@pytest.fixture +def anyio_backend() -> str: + """AnyIO Backend.""" + return "asyncio" diff --git a/.conflict-base-0/tests/logging/__init__.py b/.conflict-base-0/tests/logging/__init__.py new file mode 100644 index 0000000..a1c677a --- /dev/null +++ b/.conflict-base-0/tests/logging/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Logging Tests.""" diff --git a/.conflict-base-0/tests/logging/test_loguru.py b/.conflict-base-0/tests/logging/test_loguru.py new file mode 100644 index 0000000..9214250 --- /dev/null +++ b/.conflict-base-0/tests/logging/test_loguru.py @@ -0,0 +1,274 @@ +"""Test Logging Loguru.""" + +from collections.abc import Generator +from datetime import datetime +from io import StringIO + +import pytest +from loguru import logger +from pydantic import TypeAdapter + +from grelmicro.errors import DependencyNotFoundError +from grelmicro.logging.errors import LoggingSettingsValidationError +from grelmicro.logging.loguru import ( + JSON_FORMAT, + JSONRecordDict, + configure_logging, + json_formatter, + json_patcher, +) + +json_record_type_adapter = TypeAdapter(JSONRecordDict) + + +@pytest.fixture(autouse=True) +def cleanup_handlers() -> Generator[None, None, None]: + """Cleanup logging handlers.""" + logger.configure(handlers=[]) + yield + logger.remove() + + +def generate_logs() -> int: + """Generate logs.""" + logger.debug("Hello, World!") + logger.info("Hello, World!") + logger.warning("Hello, World!") + logger.error("Hello, Alice!", user="Alice") + try: + 1 / 0 # noqa: B018 + except ZeroDivisionError: + logger.exception("Hello, Bob!") + + return 5 + + +def assert_logs(logs: str) -> None: + """Assert logs.""" + ( + info, + warning, + error, + exception, + ) = ( + json_record_type_adapter.validate_json(line) + for line in logs.splitlines()[0:4] + ) + + expected_separator = 3 + + assert info["logger"] + assert info["logger"].startswith("tests.logging.test_loguru:generate_logs:") + assert len(info["logger"].split(":")) == expected_separator + assert info["time"] == datetime.fromisoformat(info["time"]).isoformat() + assert info["level"] == "INFO" + assert info["msg"] == "Hello, World!" + assert info["thread"] == "MainThread" + assert "ctx" not in info + + assert warning["logger"] + assert warning["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(warning["logger"].split(":")) == expected_separator + assert ( + warning["time"] == datetime.fromisoformat(warning["time"]).isoformat() + ) + assert warning["level"] == "WARNING" + assert warning["msg"] == "Hello, World!" + assert warning["thread"] == "MainThread" + assert "ctx" not in warning + + assert error["logger"] + assert error["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(error["logger"].split(":")) == expected_separator + assert error["time"] == datetime.fromisoformat(error["time"]).isoformat() + assert error["level"] == "ERROR" + assert error["msg"] == "Hello, Alice!" + assert error["thread"] == "MainThread" + assert error["ctx"] == {"user": "Alice"} + + assert exception["logger"] + assert exception["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(exception["logger"].split(":")) == expected_separator + assert ( + exception["time"] + == datetime.fromisoformat(exception["time"]).isoformat() + ) + assert exception["level"] == "ERROR" + assert exception["msg"] == "Hello, Bob!" + assert exception["thread"] == "MainThread" + assert exception["ctx"] == { + "exception": "ZeroDivisionError: division by zero", + } + + +def test_json_formatter() -> None: + """Test JSON Formatter.""" + # Arrange + sink = StringIO() + + # Act + logger.add(sink, format=json_formatter, level="INFO") + generate_logs() + + # Assert + assert_logs(sink.getvalue()) + + +def test_json_patching() -> None: + """Test JSON Patching.""" + # Arrange + sink = StringIO() + + # Act + # logger.patch(json_patcher) -> Patch is not working using logger.configure instead + logger.configure(patcher=json_patcher) + logger.add(sink, format=JSON_FORMAT, level="INFO") + generate_logs() + + # Assert + assert_logs(sink.getvalue()) + + +def test_configure_logging_default( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Default.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + configure_logging() + generate_logs() + + # Assert + assert_logs(capsys.readouterr().out) + + +def test_configure_logging_text( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Text.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "text") + + # Act + configure_logging() + generate_logs() + + # Assert + lines = capsys.readouterr().out.splitlines() + + assert "tests.logging.test_loguru:generate_logs:" in lines[0] + assert " | INFO | " in lines[0] + assert " - Hello, World!" in lines[0] + + assert "tests.logging.test_loguru:generate_logs:" in lines[1] + assert " | WARNING | " in lines[1] + assert " - Hello, World!" in lines[1] + + assert "tests.logging.test_loguru:generate_logs:" in lines[2] + assert " | ERROR | " in lines[2] + assert " - Hello, Alice!" in lines[2] + + assert "tests.logging.test_loguru:generate_logs:" in lines[3] + assert " | ERROR | " in lines[3] + assert " - Hello, Bob!" in lines[3] + assert "Traceback" in lines[4] + assert "ZeroDivisionError: division by zero" in lines[-1] + + +def test_configure_logging_json( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging JSON.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "json") + + # Act + configure_logging() + generate_logs() + + # Assert + assert_logs(capsys.readouterr().out) + + +def test_configure_logging_level( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Level.""" + # Arrange + monkeypatch.setenv("LOG_LEVEL", "DEBUG") + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + configure_logging() + logs_count = generate_logs() + + # Assert + assert len(capsys.readouterr().out.splitlines()) == logs_count + + +def test_configure_logging_invalid_level( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Invalid Level.""" + # Arrange + monkeypatch.setenv("LOG_LEVEL", "INVALID") + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + with pytest.raises( + LoggingSettingsValidationError, + match=( + r"Could not validate environment variables settings:\n" + r"- LOG_LEVEL: Input should be 'DEBUG', 'INFO', 'WARNING', 'ERROR' or 'CRITICAL'" + r" \[input=INVALID\]" + ), + ): + configure_logging() + + # Assert + assert not capsys.readouterr().out + + +def test_configure_logging_format_template( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Format Template.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "{level}: {message}") + + # Act + configure_logging() + generate_logs() + + # Assert + lines = capsys.readouterr().out.splitlines() + assert "INFO: Hello, World!" in lines[0] + assert "WARNING: Hello, World!" in lines[1] + assert "ERROR: Hello, Alice!" in lines[2] + assert "ERROR: Hello, Bob!" in lines[3] + assert "Traceback" in lines[4] + assert "ZeroDivisionError: division by zero" in lines[-1] + + +def test_configure_logging_dependency_not_found( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Configure Logging Dependency Not Found.""" + # Arrange + monkeypatch.setattr("grelmicro.logging.loguru.loguru", None) + + # Act / Assert + with pytest.raises(DependencyNotFoundError, match="loguru"): + configure_logging() diff --git a/.conflict-base-0/tests/sync/__init__.py b/.conflict-base-0/tests/sync/__init__.py new file mode 100644 index 0000000..5e3b5c4 --- /dev/null +++ b/.conflict-base-0/tests/sync/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Synchronization Primitives Tests.""" diff --git a/.conflict-base-0/tests/sync/test_backends.py b/.conflict-base-0/tests/sync/test_backends.py new file mode 100644 index 0000000..b08a92f --- /dev/null +++ b/.conflict-base-0/tests/sync/test_backends.py @@ -0,0 +1,370 @@ +"""Test Synchronization Backends.""" + +from collections.abc import AsyncGenerator, Callable, Generator +from uuid import uuid4 + +import pytest +from anyio import sleep +from testcontainers.core.container import DockerContainer +from testcontainers.postgres import PostgresContainer +from testcontainers.redis import RedisContainer + +from grelmicro.sync._backends import get_sync_backend, loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import BackendNotLoadedError +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.sync.postgres import PostgresSyncBackend +from grelmicro.sync.redis import RedisSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(15)] + + +@pytest.fixture(scope="module") +def anyio_backend() -> str: + """AnyIO Backend Module Scope.""" + return "asyncio" + + +@pytest.fixture(scope="module") +def monkeypatch() -> Generator[pytest.MonkeyPatch, None, None]: + """Monkeypatch Module Scope.""" + monkeypatch = pytest.MonkeyPatch() + yield monkeypatch + monkeypatch.undo() + + +@pytest.fixture +def clean_registry() -> Generator[None, None, None]: + """Make sure the registry is clean.""" + loaded_backends.pop("lock", None) + yield + loaded_backends.pop("lock", None) + + +@pytest.fixture( + params=[ + "memory", + pytest.param("redis", marks=[pytest.mark.integration]), + pytest.param("postgres", marks=[pytest.mark.integration]), + ], + scope="module", +) +def backend_name(request: pytest.FixtureRequest) -> str: + """Backend Name.""" + return request.param + + +@pytest.fixture( + scope="module", +) +def container( + backend_name: str, + monkeypatch: pytest.MonkeyPatch, +) -> Generator[DockerContainer | None, None, None]: + """Test Container for each Backend.""" + if backend_name == "redis": + with RedisContainer() as container: + yield container + elif backend_name == "postgres": + monkeypatch.setenv("POSTGRES_HOST", "localhost") + monkeypatch.setenv("POSTGRES_PORT", "5432") + monkeypatch.setenv("POSTGRES_DB", "test") + monkeypatch.setenv("POSTGRES_USER", "test") + monkeypatch.setenv("POSTGRES_PASSWORD", "test") + with PostgresContainer() as container: + yield container + elif backend_name == "memory": + yield None + + +@pytest.fixture(scope="module") +async def backend( + backend_name: str, container: DockerContainer | None +) -> AsyncGenerator[SyncBackend]: + """Test Container for each Backend.""" + if backend_name == "redis" and container: + port = container.get_exposed_port(6379) + async with RedisSyncBackend(f"redis://localhost:{port}/0") as backend: + yield backend + elif backend_name == "postgres" and container: + port = container.get_exposed_port(5432) + async with PostgresSyncBackend( + f"postgresql://test:test@localhost:{port}/test" + ) as backend: + yield backend + elif backend_name == "memory": + async with MemorySyncBackend() as backend: + yield backend + + +async def test_acquire(backend: SyncBackend) -> None: + """Test acquire.""" + # Arrange + name = "test_acquire" + token = uuid4().hex + duration = 1 + + # Act + result = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result + + +async def test_acquire_reantrant(backend: SyncBackend) -> None: + """Test acquire is reantrant.""" + # Arrange + name = "test_acquire_reantrant" + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result1 + assert result2 + + +async def test_acquire_already_acquired(backend: SyncBackend) -> None: + """Test acquire when already acquired.""" + # Arrange + name = "test_acquire_already_acquired" + token1 = uuid4().hex + token2 = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token1, duration=duration) + result2 = await backend.acquire(name=name, token=token2, duration=duration) + + # Assert + assert token1 != token2 + assert result1 + assert not result2 + + +async def test_acquire_expired(backend: SyncBackend) -> None: + """Test acquire when expired.""" + # Arrange + name = "test_acquire_expired" + token = uuid4().hex + duration = 0.01 + + # Act + result = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result + assert result2 + + +async def test_acquire_already_acquired_expired(backend: SyncBackend) -> None: + """Test acquire when already acquired but expired.""" + # Arrange + name = "test_acquire_already_acquired_expired" + uuid4().hex + token1 = uuid4().hex + token2 = uuid4().hex + duration = 0.01 + + # Act + result = await backend.acquire(name=name, token=token1, duration=duration) + await sleep(duration * 2) + result2 = await backend.acquire(name=name, token=token2, duration=duration) + + # Assert + assert token1 != token2 + assert result + assert result2 + + +async def test_release_not_acquired(backend: SyncBackend) -> None: + """Test release when not acquired.""" + # Arrange + name = "test_release" + uuid4().hex + token = uuid4().hex + + # Act + result = await backend.release(name=name, token=token) + + # Assert + assert not result + + +async def test_release_acquired(backend: SyncBackend) -> None: + """Test release when acquired.""" + # Arrange + name = "test_release_acquired" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert result2 + + +async def test_release_not_reantrant(backend: SyncBackend) -> None: + """Test release is not reantrant.""" + # Arrange + name = "test_release_not_reantrant" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.release(name=name, token=token) + result3 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert result2 + assert not result3 + + +async def test_release_acquired_expired(backend: SyncBackend) -> None: + """Test release when acquired but expired.""" + # Arrange + name = "test_release_acquired_expired" + uuid4().hex + token = uuid4().hex + duration = 0.01 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert not result2 + + +async def test_release_not_acquired_expired(backend: SyncBackend) -> None: + """Test release when not acquired but expired.""" + # Arrange + name = "test_release_not_acquired_expired" + uuid4().hex + token = uuid4().hex + duration = 0.01 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert not result2 + + +async def test_locked(backend: SyncBackend) -> None: + """Test locked.""" + # Arrange + name = "test_locked" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + locked_before = await backend.locked(name=name) + await backend.acquire(name=name, token=token, duration=duration) + locked_after = await backend.locked(name=name) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_owned(backend: SyncBackend) -> None: + """Test owned.""" + # Arrange + name = "test_owned" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + owned_before = await backend.owned(name=name, token=token) + await backend.acquire(name=name, token=token, duration=duration) + owned_after = await backend.owned(name=name, token=token) + + # Assert + assert owned_before is False + assert owned_after is True + + +async def test_owned_another(backend: SyncBackend) -> None: + """Test owned another.""" + # Arrange + name = "test_owned_another" + uuid4().hex + token1 = uuid4().hex + token2 = uuid4().hex + duration = 1 + + # Act + owned_before = await backend.owned(name=name, token=token1) + await backend.acquire(name=name, token=token1, duration=duration) + owned_after = await backend.owned(name=name, token=token2) + + # Assert + assert owned_before is False + assert owned_after is False + + +@pytest.mark.parametrize( + "backend_factory", + [ + lambda: MemorySyncBackend(), + lambda: RedisSyncBackend("redis://localhost:6379/0"), + lambda: PostgresSyncBackend( + "postgresql://user:password@localhost:5432/db" + ), + ], +) +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend(backend_factory: Callable[[], SyncBackend]) -> None: + """Test Get Synchronization Backend.""" + # Arrange + expected_backend = backend_factory() + + # Act + backend = get_sync_backend() + + # Assert + assert backend is expected_backend + + +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend_not_loaded() -> None: + """Test Get Synchronization Backend Not Loaded.""" + # Act / Assert + with pytest.raises(BackendNotLoadedError): + get_sync_backend() + + +@pytest.mark.parametrize( + "backend_factory", + [ + lambda: MemorySyncBackend(auto_register=False), + lambda: RedisSyncBackend( + "redis://localhost:6379/0", auto_register=False + ), + lambda: PostgresSyncBackend( + "postgresql://user:password@localhost:5432/db", auto_register=False + ), + ], +) +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend_auto_register_disabled( + backend_factory: Callable[[], SyncBackend], +) -> None: + """Test Get Synchronization Backend.""" + # Arrange + backend_factory() + + # Act / Assert + with pytest.raises(BackendNotLoadedError): + get_sync_backend() diff --git a/.conflict-base-0/tests/sync/test_leaderelection.py b/.conflict-base-0/tests/sync/test_leaderelection.py new file mode 100644 index 0000000..d357daa --- /dev/null +++ b/.conflict-base-0/tests/sync/test_leaderelection.py @@ -0,0 +1,457 @@ +"""Test leader election.""" + +import math + +import pytest +from anyio import Event, create_task_group, sleep +from pydantic import ValidationError +from pytest_mock import MockerFixture + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.leaderelection import LeaderElection, LeaderElectionConfig +from grelmicro.sync.memory import MemorySyncBackend + +WORKERS = 4 +WORKER_1 = 0 +WORKER_2 = 1 +TEST_TIMEOUT = 1 + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(TEST_TIMEOUT)] + + +@pytest.fixture +def backend() -> SyncBackend: + """Return Memory Synchronization Backend.""" + return MemorySyncBackend() + + +@pytest.fixture +def configs() -> list[LeaderElectionConfig]: + """Leader election Config.""" + return [ + LeaderElectionConfig( + name="test_leader_election", + worker=f"worker_{i}", + lease_duration=0.02, + renew_deadline=0.015, + retry_interval=0.005, + error_interval=0.01, + backend_timeout=0.005, + ) + for i in range(WORKERS) + ] + + +@pytest.fixture +def leader_elections( + backend: SyncBackend, configs: list[LeaderElectionConfig] +) -> list[LeaderElection]: + """Leader elections.""" + return [ + LeaderElection(backend=backend, **configs[i].model_dump()) + for i in range(WORKERS) + ] + + +@pytest.fixture +def leader_election( + backend: SyncBackend, configs: list[LeaderElectionConfig] +) -> LeaderElection: + """Leader election.""" + return LeaderElection(backend=backend, **configs[WORKER_1].model_dump()) + + +async def wait_first_leader(leader_elections: list[LeaderElection]) -> None: + """Wait for the first leader to be elected.""" + + async def wrapper(leader_election: LeaderElection, event: Event) -> None: + """Wait for the leadership.""" + await leader_election.wait_for_leader() + event.set() + + async with create_task_group() as task_group: + event = Event() + for coroutine in leader_elections: + task_group.start_soon(wrapper, coroutine, event) + await event.wait() + task_group.cancel_scope.cancel() + + +def test_leader_election_config() -> None: + """Test leader election Config.""" + # Arrange + config = LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + lease_duration=0.01, + renew_deadline=0.008, + retry_interval=0.001, + error_interval=0.01, + backend_timeout=0.007, + ) + + # Assert + assert config.model_dump() == { + "name": "test_leader_election", + "worker": "worker_1", + "lease_duration": 0.01, + "renew_deadline": 0.008, + "retry_interval": 0.001, + "error_interval": 0.01, + "backend_timeout": 0.007, + } + + +def test_leader_election_config_defaults() -> None: + """Test leader election Config Defaults.""" + # Arrange + config = LeaderElectionConfig( + name="test_leader_election", worker="worker_1" + ) + + # Assert + assert config.model_dump() == { + "name": "test_leader_election", + "worker": "worker_1", + "lease_duration": 15, + "renew_deadline": 10, + "retry_interval": 2, + "error_interval": 30, + "backend_timeout": 5, + } + + +def test_leader_election_config_validation_errors() -> None: + """Test leader election Config Errors.""" + # Arrange + with pytest.raises( + ValidationError, + match="Renew deadline must be shorter than lease duration", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + lease_duration=15, + renew_deadline=20, + ) + with pytest.raises( + ValidationError, + match="Retry interval must be shorter than renew deadline", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + renew_deadline=10, + retry_interval=15, + ) + with pytest.raises( + ValidationError, + match="Backend timeout must be shorter than renew deadline", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + renew_deadline=10, + backend_timeout=15, + ) + + +async def test_lifecycle(leader_election: LeaderElection) -> None: + """Test leader election on worker complete lifecycle.""" + # Act + is_leader_before_start = leader_election.is_leader() + is_running_before_start = leader_election.is_running() + async with create_task_group() as tg: + await tg.start(leader_election) + is_running_after_start = leader_election.is_running() + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + tg.cancel_scope.cancel() + is_running_after_cancel = leader_election.is_running() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_cancel is False + + assert is_running_before_start is False + assert is_running_after_start is True + assert is_running_after_cancel is False + + +async def test_leader_election_context_manager( + leader_election: LeaderElection, +) -> None: + """Test leader election on worker using context manager.""" + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + async with leader_election: + is_leader_inside_context = leader_election.is_leader() + is_leader_after_context = leader_election.is_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_inside_context is True + assert is_leader_after_context is True + assert is_leader_after_cancel is False + + +async def test_leader_election_single_worker( + leader_election: LeaderElection, +) -> None: + """Test leader election on single worker.""" + # Act + async with create_task_group() as tg: + is_leader_before_start = leader_election.is_leader() + await tg.start(leader_election) + is_leader_inside_context = leader_election.is_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_inside_context is True + assert is_leader_after_cancel is False + + +async def test_leadership_abandon_on_renew_deadline_reached( + leader_election: LeaderElection, +) -> None: + """Test leader election abandons leadership when renew deadline is reached.""" + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + leader_election.config.retry_interval = math.inf + await leader_election.wait_lose_leader() + is_leader_after_not_renewed = leader_election.is_leader() + tg.cancel_scope.cancel() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_not_renewed is False + + +async def test_leadership_abandon_on_backend_failure( + leader_election: LeaderElection, + caplog: pytest.LogCaptureFixture, + mocker: MockerFixture, +) -> None: + """Test leader election abandons leadership when backend is unreachable.""" + # Arrange + caplog.set_level("WARNING") + + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + mocker.patch.object( + leader_election.backend, + "acquire", + side_effect=Exception("Backend Unreachable"), + ) + await leader_election.wait_lose_leader() + is_leader_after_not_renewed = leader_election.is_leader() + tg.cancel_scope.cancel() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_not_renewed is False + assert ( + "Leader Election lost leadership: test_leader_election (renew deadline reached)" + in caplog.messages + ) + + +async def test_unepexpected_stop( + leader_election: LeaderElection, mocker: MockerFixture +) -> None: + """Test leader election worker abandons leadership on unexpected stop.""" + + # Arrange + async def leader_election_unexpected_exception() -> None: + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + mock = mocker.patch.object( + leader_election, + "_try_acquire_or_renew", + side_effect=Exception("Unexpected Exception"), + ) + await leader_election.wait_lose_leader() + mock.reset_mock() + tg.cancel_scope.cancel() + + # Act / Assert + with pytest.raises(ExceptionGroup): + await leader_election_unexpected_exception() + + +async def test_release_on_cancel( + backend: SyncBackend, leader_election: LeaderElection, mocker: MockerFixture +) -> None: + """Test leader election on worker that releases the lock on cancel.""" + # Arrange + spy_release = mocker.spy(backend, "release") + + # Act + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + + # Assert + spy_release.assert_called_once() + + +async def test_release_failure_ignored( + backend: SyncBackend, + leader_election: LeaderElection, + mocker: MockerFixture, +) -> None: + """Test leader election on worker that ignores release failure.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Unreachable") + ) + + # Act + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + + +async def test_only_one_leader(leader_elections: list[LeaderElection]) -> None: + """Test leader election on multiple workers ensuring only one leader is elected.""" + # Act + leaders_before_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + async with create_task_group() as tg: + for leader_election in leader_elections: + await tg.start(leader_election) + await wait_first_leader(leader_elections) + leaders_after_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + tg.cancel_scope.cancel() + for leader_election in leader_elections: + await leader_election.wait_lose_leader() + leaders_after_cancel = [ + leader_election.is_leader() for leader_election in leader_elections + ] + + # Assert + assert sum(leaders_before_start) == 0 + assert sum(leaders_after_start) == 1 + assert sum(leaders_after_cancel) == 0 + + +async def test_leader_transition( + leader_elections: list[LeaderElection], +) -> None: + """Test leader election leader transition to another worker.""" + # Arrange + leaders_after_leader_election1_start = [False] * len(leader_elections) + leaders_after_all_start = [False] * len(leader_elections) + leaders_after_leader_election1_down = [False] * len(leader_elections) + + # Act + leaders_before_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + async with create_task_group() as workers_tg: + async with create_task_group() as worker1_tg: + await worker1_tg.start(leader_elections[WORKER_1]) + await leader_elections[WORKER_1].wait_for_leader() + leaders_after_leader_election1_start = [ + leader_election.is_leader() + for leader_election in leader_elections + ] + + for leader_election in leader_elections: + await workers_tg.start(leader_election) + leaders_after_all_start = [ + leader_election.is_leader() + for leader_election in leader_elections + ] + worker1_tg.cancel_scope.cancel() + + await leader_elections[WORKER_1].wait_lose_leader() + + await wait_first_leader(leader_elections) + leaders_after_leader_election1_down = [ + leader_election.is_leader() for leader_election in leader_elections + ] + workers_tg.cancel_scope.cancel() + + for leader_election in leader_elections[WORKER_2:]: + await leader_election.wait_lose_leader() + leaders_after_all_down = [ + leader_election.is_leader() for leader_election in leader_elections + ] + + # Assert + assert sum(leaders_before_start) == 0 + assert sum(leaders_after_leader_election1_start) == 1 + assert sum(leaders_after_all_start) == 1 + assert sum(leaders_after_leader_election1_down) == 1 + assert sum(leaders_after_all_down) == 0 + + assert leaders_after_leader_election1_start[WORKER_1] is True + assert leaders_after_leader_election1_down[WORKER_1] is False + + +async def test_error_interval( + backend: SyncBackend, + leader_elections: list[LeaderElection], + caplog: pytest.LogCaptureFixture, + mocker: MockerFixture, +) -> None: + """Test leader election on worker with error cooldown.""" + # Arrange + caplog.set_level("ERROR") + leader_elections[WORKER_1].config.error_interval = 1 + leader_elections[WORKER_2].config.error_interval = 0.001 + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Unreachable") + ) + + # Act + async with create_task_group() as tg: + await tg.start(leader_elections[WORKER_1]) + await sleep(0.01) + tg.cancel_scope.cancel() + leader_election1_nb_errors = sum( + 1 for record in caplog.records if record.levelname == "ERROR" + ) + caplog.clear() + + async with create_task_group() as tg: + await tg.start(leader_elections[WORKER_2]) + await sleep(0.01) + tg.cancel_scope.cancel() + leader_election2_nb_errors = sum( + 1 for record in caplog.records if record.levelname == "ERROR" + ) + + # Assert + assert leader_election1_nb_errors == 1 + assert leader_election2_nb_errors >= 1 diff --git a/.conflict-base-0/tests/sync/test_lock.py b/.conflict-base-0/tests/sync/test_lock.py new file mode 100644 index 0000000..42e0b04 --- /dev/null +++ b/.conflict-base-0/tests/sync/test_lock.py @@ -0,0 +1,506 @@ +"""Test Lock.""" + +import time +from collections.abc import AsyncGenerator + +import pytest +from anyio import WouldBlock, sleep, to_thread +from pytest_mock import MockerFixture + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import ( + LockAcquireError, + LockNotOwnedError, + LockReleaseError, + SyncBackendError, +) +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +WORKER_1 = 0 +WORKER_2 = 1 +WORKER_COUNT = 2 + +LOCK_NAME = "test_leased_lock" + + +@pytest.fixture +async def backend() -> AsyncGenerator[SyncBackend]: + """Return Memory Synchronization Backend.""" + async with MemorySyncBackend() as backend: + yield backend + + +@pytest.fixture +def locks(backend: SyncBackend) -> list[Lock]: + """Locks of multiple workers.""" + return [ + Lock( + backend=backend, + name=LOCK_NAME, + worker=f"worker_{i}", + lease_duration=0.01, + retry_interval=0.001, + ) + for i in range(WORKER_COUNT) + ] + + +@pytest.fixture +def lock(locks: list[Lock]) -> Lock: + """Lock.""" + return locks[WORKER_1] + + +async def test_lock_owned(locks: list[Lock]) -> None: + """Test Lock owned.""" + # Act + worker_1_owned_before = await locks[WORKER_1].owned() + worker_2_owned_before = await locks[WORKER_2].owned() + await locks[WORKER_1].acquire() + worker_1_owned_after = await locks[WORKER_1].owned() + worker_2_owned_after = await locks[WORKER_2].owned() + + # Assert + assert worker_1_owned_before is False + assert worker_2_owned_before is False + assert worker_1_owned_after is True + assert worker_2_owned_after is False + + +async def test_lock_from_thread_owned(locks: list[Lock]) -> None: + """Test Lock from thread owned.""" + # Arrange + worker_1_owned_before = None + worker_2_owned_before = None + worker_1_owned_after = None + worker_2_owned_after = None + + # Act + def sync() -> None: + nonlocal worker_1_owned_before + nonlocal worker_2_owned_before + nonlocal worker_1_owned_after + nonlocal worker_2_owned_after + + worker_1_owned_before = locks[WORKER_1].from_thread.owned() + worker_2_owned_before = locks[WORKER_2].from_thread.owned() + locks[WORKER_1].from_thread.acquire() + worker_1_owned_after = locks[WORKER_1].from_thread.owned() + worker_2_owned_after = locks[WORKER_2].from_thread.owned() + + await to_thread.run_sync(sync) + + # Assert + assert worker_1_owned_before is False + assert worker_2_owned_before is False + assert worker_1_owned_after is True + assert worker_2_owned_after is False + + +async def test_lock_context_manager(lock: Lock) -> None: + """Test Lock context manager.""" + # Act + locked_before = await lock.locked() + async with lock: + locked_inside = await lock.locked() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_inside is True + assert locked_after is False + + +async def test_lock_from_thread_context_manager_acquire(lock: Lock) -> None: + """Test Lock from thread context manager.""" + # Arrange + locked_before = None + locked_inside = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_inside + nonlocal locked_after + + locked_before = lock.from_thread.locked() + with lock.from_thread: + locked_inside = lock.from_thread.locked() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_inside is True + assert locked_after is False + + +async def test_lock_context_manager_wait(lock: Lock, locks: list[Lock]) -> None: + """Test Lock context manager wait.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act + locked_before = await lock.locked() + async with locks[WORKER_2]: # Wait until lock expires + locked_inside = await lock.locked() + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_inside is True + assert locked_after is False + + +async def test_lock_from_thread_context_manager_wait( + lock: Lock, locks: list[Lock] +) -> None: + """Test Lock from thread context manager wait.""" + # Arrange + locked_before = None + locked_inside = None + locked_after = None + await locks[WORKER_1].acquire() + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_inside + nonlocal locked_after + + locked_before = lock.from_thread.locked() + with locks[WORKER_2].from_thread: + locked_inside = lock.from_thread.locked() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is True + assert locked_inside is True + assert locked_after is False + + +async def test_lock_acquire(lock: Lock) -> None: + """Test Lock acquire.""" + # Act + locked_before = await lock.locked() + await lock.acquire() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_from_thread_acquire(lock: Lock) -> None: + """Test Lock from thread acquire.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_wait(lock: Lock, locks: list[Lock]) -> None: + """Test Lock acquire wait.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act + locked_before = await lock.locked() + await locks[WORKER_2].acquire() # Wait until lock expires + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_after is True + + +async def test_lock_from_thread_acquire_wait(lock: Lock) -> None: + """Test Lock from thread acquire wait.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_nowait(lock: Lock) -> None: + """Test Lock wait acquire.""" + # Act + locked_before = await lock.locked() + await lock.acquire_nowait() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_from_thread_acquire_nowait(lock: Lock) -> None: + """Test Lock from thread wait acquire.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire_nowait() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_nowait_would_block(locks: list[Lock]) -> None: + """Test Lock wait acquire would block.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act / Assert + with pytest.raises(WouldBlock): + await locks[WORKER_2].acquire_nowait() + + +async def test_lock_from_thread_acquire_nowait_would_block( + locks: list[Lock], +) -> None: + """Test Lock from thread wait acquire would block.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act / Assert + def sync() -> None: + with pytest.raises(WouldBlock): + locks[WORKER_2].from_thread.acquire_nowait() + + await to_thread.run_sync(sync) + + +async def test_lock_release(lock: Lock) -> None: + """Test Lock release.""" + # Act / Assert + with pytest.raises(LockNotOwnedError): + await lock.release() + + +async def test_lock_from_thread_release(lock: Lock) -> None: + """Test Lock from thread release.""" + + # Act / Assert + def sync() -> None: + with pytest.raises(LockNotOwnedError): + lock.from_thread.release() + + await to_thread.run_sync(sync) + + +async def test_lock_release_acquired(lock: Lock) -> None: + """Test Lock release acquired.""" + # Arrange + await lock.acquire() + + # Act + locked_before = await lock.locked() + await lock.release() + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_after is False + + +async def test_lock_from_thread_release_acquired(lock: Lock) -> None: + """Test Lock from thread release acquired.""" + # Arrange + locked_before = None + locked_after = None + + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + lock.from_thread.acquire() + + # Act + locked_before = lock.from_thread.locked() + lock.from_thread.release() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is True + assert locked_after is False + + +async def test_lock_release_expired(locks: list[Lock]) -> None: + """Test Lock release expired.""" + # Arrange + await locks[WORKER_1].acquire() + await sleep(locks[WORKER_1].config.lease_duration) + + # Act + worker_1_locked_before = await locks[WORKER_1].locked() + with pytest.raises(LockNotOwnedError): + await locks[WORKER_2].release() + + # Assert + assert worker_1_locked_before is False + + +async def test_lock_from_thread_release_expired(locks: list[Lock]) -> None: + """Test Lock from thread release expired.""" + # Arrange + worker_1_locked_before = None + + def sync() -> None: + nonlocal worker_1_locked_before + + locks[WORKER_1].from_thread.acquire() + time.sleep(locks[WORKER_1].config.lease_duration) + + # Act + worker_1_locked_before = locks[WORKER_1].from_thread.locked() + with pytest.raises(LockNotOwnedError): + locks[WORKER_2].from_thread.release() + + await to_thread.run_sync(sync) + + # Assert + assert worker_1_locked_before is False + + +async def test_lock_acquire_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock acquire backend error.""" + # Arrange + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Error") + ) + + # Act + with pytest.raises(LockAcquireError): + await lock.acquire() + + +async def test_lock_from_thread_acquire_backend_error( + backend: SyncBackend, + lock: Lock, + mocker: MockerFixture, +) -> None: + """Test Lock from thread acquire backend error.""" + # Arrange + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Error") + ) + + # Act + def sync() -> None: + with pytest.raises(LockAcquireError): + lock.from_thread.acquire() + + await to_thread.run_sync(sync) + + +async def test_lock_release_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock release backend error.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Error") + ) + + # Act + await lock.acquire() + with pytest.raises(LockReleaseError): + await lock.release() + + +async def test_lock_from_thread_release_backend_error( + backend: SyncBackend, + lock: Lock, + mocker: MockerFixture, +) -> None: + """Test Lock from thread release backend error.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Error") + ) + + # Act + def sync() -> None: + lock.from_thread.acquire() + with pytest.raises(LockReleaseError): + lock.from_thread.release() + + await to_thread.run_sync(sync) + + +async def test_lock_owned_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock owned backend error.""" + # Arrange + mocker.patch.object( + backend, "owned", side_effect=Exception("Backend Error") + ) + + # Act / Assert + with pytest.raises(SyncBackendError): + await lock.owned() + + +async def test_lock_locked_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock locked backend error.""" + # Arrange + mocker.patch.object( + backend, "locked", side_effect=Exception("Backend Error") + ) + + # Act / Assert + with pytest.raises(SyncBackendError): + await lock.locked() diff --git a/.conflict-base-0/tests/sync/test_postgres.py b/.conflict-base-0/tests/sync/test_postgres.py new file mode 100644 index 0000000..ef8dd18 --- /dev/null +++ b/.conflict-base-0/tests/sync/test_postgres.py @@ -0,0 +1,106 @@ +"""Tests for PostgreSQL Backends.""" + +import pytest + +from grelmicro.errors import OutOfContextError +from grelmicro.sync.errors import SyncSettingsValidationError +from grelmicro.sync.postgres import PostgresSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +URL = "postgres://user:password@localhost:5432/db" + + +@pytest.mark.parametrize( + "table_name", + [ + "locks table", + "%locks", + "locks;table", + "locks' OR '1'='1", + "locks; DROP TABLE users; --", + ], +) +def test_sync_backend_table_name_invalid(table_name: str) -> None: + """Test Synchronization Backend Table Name Invalid.""" + # Act / Assert + with pytest.raises( + ValueError, match="Table name '.*' is not a valid identifier" + ): + PostgresSyncBackend(url=URL, table_name=table_name) + + +async def test_sync_backend_out_of_context_errors() -> None: + """Test Synchronization Backend Out Of Context Errors.""" + # Arrange + backend = PostgresSyncBackend(url=URL) + name = "lock" + key = "token" + + # Act / Assert + with pytest.raises(OutOfContextError): + await backend.acquire(name=name, token=key, duration=1) + with pytest.raises(OutOfContextError): + await backend.release(name=name, token=key) + with pytest.raises(OutOfContextError): + await backend.locked(name=name) + with pytest.raises(OutOfContextError): + await backend.owned(name=name, token=key) + + +@pytest.mark.parametrize( + ("environs"), + [ + { + "POSTGRES_URL": "postgresql://test_user:test_password@test_host:1234/test_db" + }, + { + "POSTGRES_USER": "test_user", + "POSTGRES_PASSWORD": "test_password", + "POSTGRES_HOST": "test_host", + "POSTGRES_PORT": "1234", + "POSTGRES_DB": "test_db", + }, + ], +) +def test_postgres_env_var_settings( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test PostgreSQL Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Act + backend = PostgresSyncBackend() + + # Assert + assert ( + backend._url + == "postgresql://test_user:test_password@test_host:1234/test_db" + ) + + +@pytest.mark.parametrize( + ("environs"), + [ + { + "POSTGRES_URL": "test://test_user:test_password@test_host:1234/test_db" + }, + {"POSTGRES_USER": "test_user"}, + ], +) +def test_postgres_env_var_settings_validation_error( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test PostgreSQL Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Assert / Act + with pytest.raises( + SyncSettingsValidationError, + match=(r"Could not validate environment variables settings:\n"), + ): + PostgresSyncBackend() diff --git a/.conflict-base-0/tests/sync/test_redis.py b/.conflict-base-0/tests/sync/test_redis.py new file mode 100644 index 0000000..a14bad7 --- /dev/null +++ b/.conflict-base-0/tests/sync/test_redis.py @@ -0,0 +1,67 @@ +"""Tests for Redis Backends.""" + +import pytest + +from grelmicro.sync.errors import SyncSettingsValidationError +from grelmicro.sync.redis import RedisSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +URL = "redis://:test_password@test_host:1234/0" + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": URL}, + { + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Act + backend = RedisSyncBackend() + + # Assert + assert backend._url == URL + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": "test://:test_password@test_host:1234/0"}, + {"REDIS_PASSWORD": "test_password"}, + { + "REDIS_URL": "test://:test_password@test_host:1234/0", + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings_validation_error( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Assert / Act + with pytest.raises( + SyncSettingsValidationError, + match=(r"Could not validate environment variables settings:\n"), + ): + RedisSyncBackend() diff --git a/.conflict-base-0/tests/sync/utils.py b/.conflict-base-0/tests/sync/utils.py new file mode 100644 index 0000000..e20356b --- /dev/null +++ b/.conflict-base-0/tests/sync/utils.py @@ -0,0 +1,23 @@ +"""Test utilities for Lock.""" + +from anyio import Event, create_task_group, fail_after + +from grelmicro.sync._base import BaseLock + + +async def wait_first_acquired(locks: list[BaseLock]) -> None: + """Wait for the first lock to be acquired.""" + + async def wrapper(lock: BaseLock, event: Event) -> None: + """Send event when lock is acquired.""" + with fail_after(1): + await lock.acquire() + event.set() + + with fail_after(1): + async with create_task_group() as task_group: + event = Event() + for lock in locks: + task_group.start_soon(wrapper, lock, event) + await event.wait() + task_group.cancel_scope.cancel() diff --git a/.conflict-base-0/tests/task/__init__.py b/.conflict-base-0/tests/task/__init__.py new file mode 100644 index 0000000..ebf85b3 --- /dev/null +++ b/.conflict-base-0/tests/task/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Task Scheduler Tests.""" diff --git a/.conflict-base-0/tests/task/samples.py b/.conflict-base-0/tests/task/samples.py new file mode 100644 index 0000000..d19c153 --- /dev/null +++ b/.conflict-base-0/tests/task/samples.py @@ -0,0 +1,86 @@ +"""Test Samples for the Task Component.""" + +from types import TracebackType +from typing import Self + +from anyio import TASK_STATUS_IGNORED, Condition, Event +from anyio.abc import TaskStatus +from typer import echo + +from grelmicro.sync.abc import Synchronization +from grelmicro.task.abc import Task + +condition = Condition() + + +def test1() -> None: + """Test Function.""" + echo("test1") + + +def test2() -> None: + """Test Function.""" + + +def test3(test: str = "test") -> None: + """Test Function.""" + + +async def notify() -> None: + """Test Function that notifies the condition.""" + async with condition: + condition.notify() + + +async def always_fail() -> None: + """Test Function that always fails.""" + msg = "Test Error" + raise ValueError(msg) + + +class SimpleClass: + """Test Class.""" + + def method(self) -> None: + """Test Method.""" + + @staticmethod + def static_method() -> None: + """Test Static Method.""" + + +class EventTask(Task): + """Test Scheduled Task with Event.""" + + def __init__(self, *, event: Event | None = None) -> None: + """Initialize the event task.""" + self._event = event or Event() + + @property + def name(self) -> str: + """Return the task name.""" + return "event_task" + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run the task that sets the event.""" + task_status.started() + self._event.set() + + +class BadLock(Synchronization): + """Bad Lock.""" + + async def __aenter__(self) -> Self: + """Enter the synchronization primitive.""" + msg = "Bad Lock" + raise ValueError(msg) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the synchronization primitive.""" diff --git a/.conflict-base-0/tests/task/test_interval.py b/.conflict-base-0/tests/task/test_interval.py new file mode 100644 index 0000000..308d456 --- /dev/null +++ b/.conflict-base-0/tests/task/test_interval.py @@ -0,0 +1,127 @@ +"""Test Interval Task.""" + +import pytest +from anyio import create_task_group, sleep, sleep_forever +from pytest_mock import MockFixture + +from grelmicro.task._interval import IntervalTask +from tests.task.samples import ( + BadLock, + always_fail, + condition, + notify, + test1, +) + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] + +INTERVAL = 0.1 +SLEEP = 0.01 + + +def test_interval_task_init() -> None: + """Test Interval Task Initialization.""" + # Act + task = IntervalTask(interval=1, function=test1) + # Assert + assert task.name == "tests.task.samples:test1" + + +def test_interval_task_init_with_name() -> None: + """Test Interval Task Initialization with Name.""" + # Act + task = IntervalTask(interval=1, function=test1, name="test1") + # Assert + assert task.name == "test1" + + +def test_interval_task_init_with_invalid_interval() -> None: + """Test Interval Task Initialization with Invalid Interval.""" + # Act / Assert + with pytest.raises(ValueError, match="Interval must be greater than 0"): + IntervalTask(interval=0, function=test1) + + +async def test_interval_task_start() -> None: + """Test Interval Task Start.""" + # Arrange + task = IntervalTask(interval=1, function=notify) + # Act + async with create_task_group() as tg: + await tg.start(task) + async with condition: + await condition.wait() + tg.cancel_scope.cancel() + + +async def test_interval_task_execution_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test Interval Task Execution Error.""" + # Arrange + task = IntervalTask(interval=1, function=always_fail) + # Act + async with create_task_group() as tg: + await tg.start(task) + await sleep(SLEEP) + tg.cancel_scope.cancel() + + # Assert + assert any( + "Task execution error:" in record.message + for record in caplog.records + if record.levelname == "ERROR" + ) + + +async def test_interval_task_synchronization_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test Interval Task Synchronization Error.""" + # Arrange + task = IntervalTask(interval=1, function=notify, sync=BadLock()) + + # Act + async with create_task_group() as tg: + await tg.start(task) + await sleep(SLEEP) + tg.cancel_scope.cancel() + + # Assert + assert any( + "Task synchronization error:" in record.message + for record in caplog.records + if record.levelname == "ERROR" + ) + + +async def test_interval_stop( + caplog: pytest.LogCaptureFixture, mocker: MockFixture +) -> None: + """Test Interval Task stop.""" + # Arrange + caplog.set_level("INFO") + + class CustomBaseException(BaseException): + pass + + mocker.patch( + "grelmicro.task._interval.sleep", side_effect=CustomBaseException + ) + task = IntervalTask(interval=1, function=test1) + + async def leader_election_during_runtime_error() -> None: + async with create_task_group() as tg: + await tg.start(task) + await sleep_forever() + + # Act + with pytest.raises(BaseExceptionGroup): + await leader_election_during_runtime_error() + + # Assert + assert any( + "Task stopped:" in record.message + for record in caplog.records + if record.levelname == "INFO" + ) diff --git a/.conflict-base-0/tests/task/test_manager.py b/.conflict-base-0/tests/task/test_manager.py new file mode 100644 index 0000000..62c9859 --- /dev/null +++ b/.conflict-base-0/tests/task/test_manager.py @@ -0,0 +1,81 @@ +"""Test Task Manager.""" + +import pytest +from anyio import Event + +from grelmicro.errors import OutOfContextError +from grelmicro.task import TaskManager +from grelmicro.task.errors import TaskAddOperationError +from tests.task.samples import EventTask + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] + + +def test_task_manager_init() -> None: + """Test Task Manager Initialization.""" + # Act + task = EventTask() + app = TaskManager() + app_with_tasks = TaskManager(tasks=[task]) + # Assert + assert app.tasks == [] + assert app_with_tasks.tasks == [task] + + +async def test_task_manager_context() -> None: + """Test Task Manager Context.""" + # Arrange + event = Event() + task = EventTask(event=event) + app = TaskManager(tasks=[task]) + + # Act + event_before = event.is_set() + async with app: + event_in_context = event.is_set() + + # Assert + assert event_before is False + assert event_in_context is True + + +@pytest.mark.parametrize("auto_start", [True, False]) +async def test_task_manager_auto_start_disabled(*, auto_start: bool) -> None: + """Test Task Manager Auto Start Disabled.""" + # Arrange + event = Event() + task = EventTask(event=event) + app = TaskManager(auto_start=auto_start, tasks=[task]) + + # Act + event_before = event.is_set() + async with app: + event_in_context = event.is_set() + + # Assert + assert event_before is False + assert event_in_context is auto_start + + +async def test_task_manager_already_started_error() -> None: + """Test Task Manager Already Started Warning.""" + # Arrange + app = TaskManager() + + # Act / Assert + async with app: + with pytest.raises(TaskAddOperationError): + await app.start() + + +async def test_task_manager_out_of_context_errors() -> None: + """Test Task Manager Out of Context Errors.""" + # Arrange + app = TaskManager() + + # Act / Assert + with pytest.raises(OutOfContextError): + await app.start() + + with pytest.raises(OutOfContextError): + await app.__aexit__(None, None, None) diff --git a/.conflict-base-0/tests/task/test_router.py b/.conflict-base-0/tests/task/test_router.py new file mode 100644 index 0000000..ed30af7 --- /dev/null +++ b/.conflict-base-0/tests/task/test_router.py @@ -0,0 +1,175 @@ +"""Test Task Router.""" + +from functools import partial + +import pytest + +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.task import TaskRouter +from grelmicro.task._interval import IntervalTask +from grelmicro.task.errors import FunctionTypeError, TaskAddOperationError +from tests.task.samples import EventTask, SimpleClass, test1, test2, test3 + + +def test_router_init() -> None: + """Test Task Router Initialization.""" + # Arrange + custom_task = EventTask() + + # Act + router = TaskRouter() + router_with_task = TaskRouter(tasks=[custom_task]) + + # Assert + assert router.tasks == [] + assert router_with_task.tasks == [custom_task] + + +def test_router_add_task() -> None: + """Test Task Router Add Task.""" + # Arrange + custom_task1 = EventTask() + custom_task2 = EventTask() + router = TaskRouter() + router_with_task = TaskRouter(tasks=[custom_task1]) + + # Act + router.add_task(custom_task1) + router_with_task.add_task(custom_task2) + + # Assert + assert router.tasks == [custom_task1] + assert router_with_task.tasks == [custom_task1, custom_task2] + + +def test_router_include_router() -> None: + """Test Task Router Include Router.""" + # Arrange + custom_task1 = EventTask() + custom_task2 = EventTask() + router = TaskRouter(tasks=[custom_task1]) + router_with_task = TaskRouter(tasks=[custom_task2]) + + # Act + router.include_router(router_with_task) + + # Assert + assert router.tasks == [custom_task1, custom_task2] + + +def test_router_interval() -> None: + """Test Task Router add interval task.""" + # Arrange + task_count = 4 + custom_task = EventTask() + router = TaskRouter(tasks=[custom_task]) + sync = Lock(backend=MemorySyncBackend(), name="testlock") + + # Act + router.interval(name="test1", seconds=10, sync=sync)(test1) + router.interval(name="test2", seconds=20)(test2) + router.interval(seconds=10)(test3) + + # Assert + assert len(router.tasks) == task_count + assert ( + sum(isinstance(task, IntervalTask) for task in router.tasks) + == task_count - 1 + ) + assert router.tasks[0].name == "event_task" + assert router.tasks[1].name == "test1" + assert router.tasks[2].name == "test2" + assert router.tasks[3].name == "tests.task.samples:test3" + + +def test_router_interval_name_generation() -> None: + """Test Task Router Interval Name Generation.""" + # Arrange + router = TaskRouter() + + # Act + router.interval(seconds=10)(test1) + router.interval(seconds=10)(SimpleClass.static_method) + router.interval(seconds=10)(SimpleClass.method) + + # Assert + assert router.tasks[0].name == "tests.task.samples:test1" + assert ( + router.tasks[1].name == "tests.task.samples:SimpleClass.static_method" + ) + assert router.tasks[2].name == "tests.task.samples:SimpleClass.method" + + +def test_router_interval_name_generation_error() -> None: + """Test Task Router Interval Name Generation Error.""" + # Arrange + router = TaskRouter() + test_instance = SimpleClass() + + # Act + with pytest.raises(FunctionTypeError, match="nested function"): + + @router.interval(seconds=10) + def nested_function() -> None: + pass + + with pytest.raises(FunctionTypeError, match="lambda"): + router.interval(seconds=10)(lambda _: None) + + with pytest.raises(FunctionTypeError, match="method"): + router.interval(seconds=10)(test_instance.method) + + with pytest.raises(FunctionTypeError, match="partial()"): + router.interval(seconds=10)(partial(test1)) + + with pytest.raises( + FunctionTypeError, + match="callable without __module__ or __qualname__ attribute", + ): + router.interval(seconds=10)(object()) # type: ignore[arg-type] + + +def test_router_add_task_when_started() -> None: + """Test Task Router Add Task When Started.""" + # Arrange + custom_task = EventTask() + router = TaskRouter() + router.do_mark_as_started() + + # Act + with pytest.raises(TaskAddOperationError): + router.add_task(custom_task) + + +def test_router_include_router_when_started() -> None: + """Test Task Router Include Router When Started.""" + # Arrange + router = TaskRouter() + router.do_mark_as_started() + router_child = TaskRouter() + + # Act + with pytest.raises(TaskAddOperationError): + router.include_router(router_child) + + +def test_router_started_propagation() -> None: + """Test Task Router Started Propagation.""" + # Arrange + router = TaskRouter() + router_child = TaskRouter() + router.include_router(router_child) + + # Act + router_started_before = router.started() + router_child_started_before = router_child.started() + router.do_mark_as_started() + router_started_after = router.started() + router_child_started_after = router_child.started() + + # Assert + assert router_started_before is False + assert router_child_started_before is False + assert router_started_after is True + assert router_child_started_after is True diff --git a/.conflict-base-0/uv.lock b/.conflict-base-0/uv.lock new file mode 100644 index 0000000..ff11a2b --- /dev/null +++ b/.conflict-base-0/uv.lock @@ -0,0 +1,1934 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version < '3.13'", + "python_full_version >= '3.13'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, + { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, + { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, + { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, + { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, + { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, + { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, + { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 }, + { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 }, + { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 }, + { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 }, + { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 }, + { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 }, + { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 }, + { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 }, + { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 }, + { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 }, + { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 }, + { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 }, + { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 }, + { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 }, + { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 }, + { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 }, + { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, + { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, + { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, + { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, + { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, + { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, + { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, + { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, + { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, + { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, + { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, + { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, + { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, + { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, + { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, + { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, + { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, + { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, + { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, + { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, + { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, + { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, + { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "43.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, + { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, + { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, + { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, + { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, + { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, + { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, + { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, + { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, + { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, + { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, + { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, +] + +[[package]] +name = "cyclic" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, +] + +[[package]] +name = "fast-depends" +version = "2.4.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/f5/8b42b7588a67ad78991e5e7ca0e0c6a1ded535a69a725e4e48d3346a20c1/fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d", size = 16682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/08/4adb160d8394053289fdf3b276e93b53271fd463e54fff8911b23c1db4ed/fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2", size = 17651 }, +] + +[[package]] +name = "fastapi" +version = "0.115.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/29/f71316b9273b6552a263748e49cd7b83898dc9499a663d30c7b9cb853cb8/fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", size = 301047 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c4/148d5046a96c428464557264877ae5a9338a83bbe0df045088749ec89820/fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796", size = 94866 }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/1ad5ce32d029aeb9117e9a5a9b3e314a8477525d60c12a9b7730a3c186ec/fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", size = 15571 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/ea/4b5011012ac925fe2f83b19d0e09cee9d324141ec7bf5e78bb2817f96513/fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46", size = 9489 }, +] + +[[package]] +name = "faststream" +version = "0.5.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "fast-depends" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/d3/c2a3e1233274c93a4978cbac210a81ba05cee09e2e0051049b40f55406f1/faststream-0.5.30.tar.gz", hash = "sha256:50ad5288719cfa75c13e9c277d40afae62533a590facad6e6d215e868f2b97f4", size = 284478 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/ce/a9eec6c2c9803de6bc2b2a5cac35d56b8908c64fcdd4c73616c1a16c9b90/faststream-0.5.30-py3-none-any.whl", hash = "sha256:bf48826be99210f3e9c7dff1b2a17b4bc4762c873c5558ac81b9b873549ae6a1", size = 382011 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "grelmicro" +version = "0.2.2" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "fast-depends" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, +] + +[package.optional-dependencies] +postgres = [ + { name = "asyncpg" }, +] +redis = [ + { name = "redis" }, +] +standard = [ + { name = "loguru" }, + { name = "orjson" }, +] + +[package.dev-dependencies] +dev = [ + { name = "fastapi" }, + { name = "fastapi-cli" }, + { name = "faststream" }, + { name = "hatch" }, + { name = "mdx-include" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-randomly" }, + { name = "pytest-timeout" }, + { name = "ruff" }, + { name = "testcontainers", extra = ["redis"] }, +] +docs = [ + { name = "mkdocs-material" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.0.0" }, + { name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.30.0" }, + { name = "fast-depends", specifier = ">=2.0.0" }, + { name = "loguru", marker = "extra == 'standard'", specifier = ">=0.7.2" }, + { name = "orjson", marker = "extra == 'standard'", specifier = ">=3.10.11" }, + { name = "pydantic", specifier = ">=2.5.0" }, + { name = "pydantic-settings", specifier = ">=2.5.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "fastapi", specifier = ">=0.115.5" }, + { name = "fastapi-cli", specifier = ">=0.0.5" }, + { name = "faststream", specifier = ">=0.5.30" }, + { name = "hatch", specifier = ">=1.13.0" }, + { name = "mdx-include", specifier = ">=1.4.2" }, + { name = "mypy", specifier = ">=1.12.0" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-mock", specifier = ">=3.14.0" }, + { name = "pytest-randomly", specifier = ">=3.16.0" }, + { name = "pytest-timeout", specifier = ">=2.3.1" }, + { name = "ruff", specifier = ">=0.7.4" }, + { name = "testcontainers", extras = ["postgres", "redis"], specifier = ">=4.8.2" }, +] +docs = [ + { name = "mkdocs-material", specifier = ">=9.5.44" }, + { name = "pygments", specifier = ">=2.18.0" }, + { name = "pymdown-extensions", specifier = ">=10.12" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "hatch" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "hyperlink" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "pexpect" }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "userpath" }, + { name = "uv" }, + { name = "virtualenv" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/ed/5001de278f8d7381cbc84f5efdae72308fe37493bc063878f6a1ac07dab8/hatch-1.13.0.tar.gz", hash = "sha256:5e1a75770cfe8f3ebae3abfded3a976238b0acefd19cdabc5245597525b8066f", size = 5188060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/8d/6d965a22bc38cec091ba82131624bb5d75471094d7fe05e829536de3de2f/hatch-1.13.0-py3-none-any.whl", hash = "sha256:bb1a18558a626279cae338b4d8a9d3ca4226d5e06d50de600608c57acd131b67", size = 125757 }, +] + +[[package]] +name = "hatchling" +version = "1.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/47/7ec270a9567262ae3cb32dd420d2b53bf7aee769aca1f240eae0426b5bbc/hatchling-1.26.3.tar.gz", hash = "sha256:b672a9c36a601a06c4e88a1abb1330639ee8e721e0535a37536e546a667efc7a", size = 54968 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl", hash = "sha256:c407e1c6c17b574584a66ae60e8e9a01235ecb6dc61d01559bb936577aaf5846", size = 75773 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, +] + +[[package]] +name = "identify" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/79/7a520fc5011e02ca3f3285b5f6820eaf80443eb73e3733f73c02fb42ba0b/identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd", size = 99113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/86/c4395700f3c5475424fb5c41e20c16be28d10c904aee4d005ba3217fc8e7/identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", size = 98982 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, +] + +[[package]] +name = "jeepney" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "keyring" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/24/64447b13df6a0e2797b586dad715766d756c932ce8ace7f67bd384d76ae0/keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6", size = 62675 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c9/353c156fa2f057e669106e5d6bcdecf85ef8d3536ce68ca96f18dc7b6d6f/keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741", size = 39096 }, +] + +[[package]] +name = "loguru" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549 }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mdx-include" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cyclic" }, + { name = "markdown" }, + { name = "rcslice" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591 }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.5.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/56/182d8121db9ab553cdf9bc58d5972b89833f60b63272f693c1f2b849b640/mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0", size = 3964306 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/eb/a801d00e0e210d82184aacce596906ec065422c78a7319244ba0771c4ded/mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca", size = 8674509 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, +] + +[[package]] +name = "mypy" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/03/744330105a74dc004578f47ec27e1bf66b1dd5664ea444d18423e41343bd/mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d", size = 3150767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/0a/70de7c97a86cb85535077ab5cef1cbc4e2812fd2e9cc21d78eb561a6b80f/mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", size = 10940998 }, + { url = "https://files.pythonhosted.org/packages/c0/97/9ed6d4834d7549936ab88533b302184fb568a0940c4000d2aaee6dc07112/mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", size = 10108523 }, + { url = "https://files.pythonhosted.org/packages/48/41/1686f37d09c915dfc5b683e20cc99dabac199900b5ca6d22747b99ddcb50/mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", size = 12505553 }, + { url = "https://files.pythonhosted.org/packages/8d/2b/2dbcaa7e97b23f27ced77493256ee878f4a140ac750e198630ff1b9b60c6/mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", size = 12988634 }, + { url = "https://files.pythonhosted.org/packages/54/55/710d082e91a2ccaea21214229b11f9215a9d22446f949491b5457655e82b/mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", size = 9630747 }, + { url = "https://files.pythonhosted.org/packages/8a/74/b9e0e4f06e951e277058f878302faa154d282ca11274c59fe08353f52949/mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", size = 11079902 }, + { url = "https://files.pythonhosted.org/packages/9f/62/fcad290769db3eb0de265094cef5c94d6075c70bc1e42b67eee4ca192dcc/mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", size = 10072373 }, + { url = "https://files.pythonhosted.org/packages/cb/27/9ac78349c2952e4446288ec1174675ab9e0160ed18c2cb1154fa456c54e8/mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", size = 12589779 }, + { url = "https://files.pythonhosted.org/packages/7c/4a/58cebd122cf1cba95680ac51303fbeb508392413ca64e3e711aa7d4877aa/mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", size = 13044459 }, + { url = "https://files.pythonhosted.org/packages/5b/c7/672935e2a3f9bcc07b1b870395a653f665657bef3cdaa504ad99f56eadf0/mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", size = 9731919 }, + { url = "https://files.pythonhosted.org/packages/bb/b0/092be5094840a401940c95224f63bb2a8f09bce9251ac1df180ec523830c/mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", size = 11068611 }, + { url = "https://files.pythonhosted.org/packages/9a/86/f20f53b8f062876c39602243d7a59b5cabd6b24315d8de511d607fa4de6a/mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", size = 10068036 }, + { url = "https://files.pythonhosted.org/packages/84/c7/1dbd6575785522da1d4c1ac2c419505fcf23bee74811880cac447a4a77ab/mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", size = 12585671 }, + { url = "https://files.pythonhosted.org/packages/46/8a/f6ae18b446eb2bccce54c4bd94065bcfe417d6c67021dcc032bf1e720aff/mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", size = 13036083 }, + { url = "https://files.pythonhosted.org/packages/59/e6/fc65fde3dc7156fce8d49ba21c7b1f5d866ad50467bf196ca94a7f6d2c9e/mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", size = 9735467 }, + { url = "https://files.pythonhosted.org/packages/84/6b/1db9de4e0764778251fb2d64cb7455cf6db75dc99c9f72c8b7e74b6a8a17/mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", size = 2646060 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "orjson" +version = "3.10.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/3a/10320029954badc7eaa338a15ee279043436f396e965dafc169610e4933f/orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", size = 5444879 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/25/c869a1fbd481dcb02c70032fd6a7243de7582bc48c7cae03d6f0985a11c0/orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6", size = 266432 }, + { url = "https://files.pythonhosted.org/packages/6a/a4/2307155ee92457d28345308f7d8c0e712348404723025613adeffcb531d0/orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe", size = 151884 }, + { url = "https://files.pythonhosted.org/packages/aa/82/daf1b2596dd49fe44a1bd92367568faf6966dcb5d7f99fd437c3d0dc2de6/orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67", size = 167371 }, + { url = "https://files.pythonhosted.org/packages/63/a8/680578e4589be5fdcfe0186bdd7dc6fe4a39d30e293a9da833cbedd5a56e/orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b", size = 154368 }, + { url = "https://files.pythonhosted.org/packages/6e/ce/9cb394b5b01ef34579eeca6d704b21f97248f607067ce95a24ba9ea2698e/orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d", size = 165725 }, + { url = "https://files.pythonhosted.org/packages/49/24/55eeb05cfb36b9e950d05743e6f6fdb7d5f33ca951a27b06ea6d03371aed/orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5", size = 142522 }, + { url = "https://files.pythonhosted.org/packages/94/0c/3a6a289e56dcc9fe67dc6b6d33c91dc5491f9ec4a03745efd739d2acf0ff/orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a", size = 146934 }, + { url = "https://files.pythonhosted.org/packages/1d/5c/a08c0e90a91e2526029a4681ff8c6fc4495b8bab77d48801144e378c7da9/orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981", size = 142904 }, + { url = "https://files.pythonhosted.org/packages/2c/c9/710286a60b14e88288ca014d43befb08bb0a4a6a0f51b875f8c2f05e8205/orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55", size = 144459 }, + { url = "https://files.pythonhosted.org/packages/7d/68/ef7b920e0a09e02b1a30daca1b4864938463797995c2fabe457c1500220a/orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec", size = 136444 }, + { url = "https://files.pythonhosted.org/packages/78/f2/a712dbcef6d84ff53e13056e7dc69d9d4844bd1e35e51b7431679ddd154d/orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51", size = 266505 }, + { url = "https://files.pythonhosted.org/packages/94/54/53970831786d71f98fdc13c0f80451324c9b5c20fbf42f42ef6147607ee7/orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97", size = 151745 }, + { url = "https://files.pythonhosted.org/packages/35/38/482667da1ca7ef95d44d4d2328257a144fd2752383e688637c53ed474d2a/orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19", size = 167274 }, + { url = "https://files.pythonhosted.org/packages/23/2f/5bb0a03e819781d82dadb733fde8ebbe20d1777d1a33715d45ada4d82ce8/orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0", size = 154605 }, + { url = "https://files.pythonhosted.org/packages/49/e9/14cc34d45c7bd51665aff9b1bb6b83475a61c52edb0d753fffe1adc97764/orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433", size = 165874 }, + { url = "https://files.pythonhosted.org/packages/7b/61/c2781ecf90f99623e97c67a31e8553f38a1ecebaf3189485726ac8641576/orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5", size = 142813 }, + { url = "https://files.pythonhosted.org/packages/4d/4f/18c83f78b501b6608569b1610fcb5a25c9bb9ab6a7eb4b3a55131e0fba37/orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd", size = 146762 }, + { url = "https://files.pythonhosted.org/packages/ba/19/ea80d5b575abd3f76a790409c2b7b8a60f3fc9447965c27d09613b8bddf4/orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b", size = 143186 }, + { url = "https://files.pythonhosted.org/packages/2c/f5/d835fee01a0284d4b78effc24d16e7609daac2ff6b6851ca1bdd3b6194fc/orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d", size = 144489 }, + { url = "https://files.pythonhosted.org/packages/03/60/748e0e205060dec74328dfd835e47902eb5522ae011766da76bfff64e2f4/orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284", size = 136614 }, + { url = "https://files.pythonhosted.org/packages/13/92/400970baf46b987c058469e9e779fb7a40d54a5754914d3634cca417e054/orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", size = 266402 }, + { url = "https://files.pythonhosted.org/packages/3c/fa/f126fc2d817552bd1f67466205abdcbff64eab16f6844fe6df2853528675/orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", size = 140826 }, + { url = "https://files.pythonhosted.org/packages/ad/18/9b9664d7d4af5b4fe9fe6600b7654afc0684bba528260afdde10c4a530aa/orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", size = 142593 }, + { url = "https://files.pythonhosted.org/packages/20/f9/a30c68f12778d5e58e6b5cdd26f86ee2d0babce1a475073043f46fdd8402/orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", size = 146777 }, + { url = "https://files.pythonhosted.org/packages/f2/97/12047b0c0e9b391d589fb76eb40538f522edc664f650f8e352fdaaf77ff5/orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", size = 142961 }, + { url = "https://files.pythonhosted.org/packages/a4/97/d904e26c1cabf2dd6ab1b0909e9b790af28a7f0fcb9d8378d7320d4869eb/orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", size = 144486 }, + { url = "https://files.pythonhosted.org/packages/42/62/3760bd1e6e949321d99bab238d08db2b1266564d2f708af668f57109bb36/orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", size = 136361 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, +] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, + { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, + { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, + { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, + { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, + { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, + { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, + { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, + { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, + { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, + { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, + { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, + { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, + { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, + { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, + { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, + { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, + { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, + { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, + { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, + { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, + { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, + { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, + { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, + { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, + { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, + { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, + { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, + { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, + { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, + { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, + { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, + { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, + { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, + { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/32f05854cfd432e9286bb41a870e0d1a926b72df5f5cdb6dec962b2e369e/pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7", size = 840790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/32/95a164ddf533bd676cbbe878e36e89b4ade3efde8dd61d0148c90cbbe57e/pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", size = 263448 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "pytest-randomly" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396 }, +] + +[[package]] +name = "pytest-timeout" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "pywin32" +version = "308" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156 }, + { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559 }, + { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495 }, + { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, + { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, + { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, + { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, + { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, + { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, +] + +[[package]] +name = "rcslice" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180 }, +] + +[[package]] +name = "redis" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "13.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", size = 222889 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", size = 242157 }, +] + +[[package]] +name = "ruff" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, + { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, + { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, + { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, + { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, + { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, + { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, + { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, + { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, + { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, + { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, + { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, + { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, + { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, + { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, + { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.41.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, +] + +[[package]] +name = "testcontainers" +version = "4.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/72/c58d84f5704c6caadd9f803a3adad5ab54ac65328c02d13295f40860cf33/testcontainers-4.8.2.tar.gz", hash = "sha256:dd4a6a2ea09e3c3ecd39e180b6548105929d0bb78d665ce9919cb3f8c98f9853", size = 63590 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/77/5ac0dff2903a033d83d971fd85957356abdb66a327f3589df2b3d1a586b4/testcontainers-4.8.2-py3-none-any.whl", hash = "sha256:9e19af077cd96e1957c13ee466f1f32905bc6c5bc1bc98643eb18be1a989bfb0", size = 104326 }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "tomli" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, +] + +[[package]] +name = "tomli-w" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + +[[package]] +name = "trove-classifiers" +version = "2024.10.21.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/85/92c2667cf221b37648041ce9319427f92fa76cbec634aad844e67e284706/trove_classifiers-2024.10.21.16.tar.gz", hash = "sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3", size = 16153 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl", hash = "sha256:0fb11f1e995a757807a8ef1c03829fbd4998d817319abcef1f33165750f103be", size = 13546 }, +] + +[[package]] +name = "typer" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "userpath" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065 }, +] + +[[package]] +name = "uv" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/ad/66cc8e00c217e7fcf76598c880632b480aa38d4cad311596b78e99737498/uv-0.5.4.tar.gz", hash = "sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00", size = 2315678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/3e/6bf24d7bb0d11715ea783ecabcacdecdc8c51fca0144fcdad2090d65bae5/uv-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2", size = 13853445 }, + { url = "https://files.pythonhosted.org/packages/b8/be/c3acbe2944cd694a5d61a7a461468fa886512c84014545bb8f3244092eaa/uv-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58", size = 13969300 }, + { url = "https://files.pythonhosted.org/packages/1f/c5/06e3b93045179b92d75cf94e6e224baec3226070f1cbc0e11d4898300b54/uv-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4", size = 12932325 }, + { url = "https://files.pythonhosted.org/packages/b8/f9/06ab86e9f0c270c495077ef2b588458172ed84f9c337de725c8b08872354/uv-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39", size = 13183356 }, + { url = "https://files.pythonhosted.org/packages/c1/cb/bee01ef23e5020dc1f12d86ca8f82e95a723585db3ec64bfab4016e5616c/uv-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6", size = 13622310 }, + { url = "https://files.pythonhosted.org/packages/19/4b/128fd874151919c71af51f528db28964e6d8e509fff12210ec9ba99b13fb/uv-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263", size = 14207832 }, + { url = "https://files.pythonhosted.org/packages/b1/2b/0fed8a49440494f6806dcb67021ca8f14d46f45a665235fc153791e19574/uv-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83", size = 14878796 }, + { url = "https://files.pythonhosted.org/packages/c9/35/a6dc404d4d8884e26ad7bda004c101972fe7d81f86546a8628272812b897/uv-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25", size = 14687838 }, + { url = "https://files.pythonhosted.org/packages/74/9e/c2ebf66b90d48def06cda29626bb38068418ed135ca903beb293825ef66d/uv-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927", size = 18960541 }, + { url = "https://files.pythonhosted.org/packages/3d/67/28a8b4c23920ae1b1b0103ebae2fa176bd5677c4353b5e814a51bd183285/uv-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194", size = 14471756 }, + { url = "https://files.pythonhosted.org/packages/e9/1c/9698818f4c5493dfd5ab0899a90eee789cac214de2f171220bcdfaefc93a/uv-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01", size = 13389089 }, + { url = "https://files.pythonhosted.org/packages/0b/30/31a9985d84ffb63fb9212fa2b565497e0ceb581be055e5cc760afbe26b11/uv-0.5.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c", size = 13612748 }, + { url = "https://files.pythonhosted.org/packages/26/8d/bae613187ba88d74f0268246ce140f23d399bab96d2cbc055d6e4adafd09/uv-0.5.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876", size = 13946421 }, + { url = "https://files.pythonhosted.org/packages/0e/22/efd1eec81a566139bced68f4bd140c275edac3dac1bd6236cf8d756423db/uv-0.5.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119", size = 15752913 }, + { url = "https://files.pythonhosted.org/packages/49/b2/0cc4ae143b9605c25e75772aea22876b5875db79982ba62bb6f8d3099fab/uv-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc", size = 14599503 }, + { url = "https://files.pythonhosted.org/packages/51/9a/33d40a5068fd37c4f7b4fa82396e3ee90a691cd256f364ff398612c1d5d4/uv-0.5.4-py3-none-win32.whl", hash = "sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa", size = 13749570 }, + { url = "https://files.pythonhosted.org/packages/b1/c8/827e4da65cbdab2c1619767a68ab99a31de078e511b71ca9f24777df33f9/uv-0.5.4-py3-none-win_amd64.whl", hash = "sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb", size = 15573613 }, +] + +[[package]] +name = "uvicorn" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + +[[package]] +name = "virtualenv" +version = "20.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + +[[package]] +name = "watchfiles" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, + { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, + { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, + { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, + { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, + { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, + { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, + { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, + { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, + { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, + { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, + { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, + { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, + { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, + { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, + { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, + { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, + { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, + { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, + { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, + { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, + { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, + { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, + { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, + { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, + { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, + { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, + { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, + { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, + { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, + { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, + { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, + { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, + { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, + { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, + { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, + { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, +] + +[[package]] +name = "websockets" +version = "14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, + { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, + { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, + { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, + { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, + { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, + { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, + { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, + { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, + { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, + { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, + { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, + { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, + { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, + { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, + { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, + { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, + { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, + { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, + { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, + { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, + { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, + { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, + { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, + { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, + { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, + { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, + { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", size = 3676 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad", size = 3604 }, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313 }, + { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164 }, + { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890 }, + { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118 }, + { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746 }, + { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556 }, + { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712 }, + { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327 }, + { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523 }, + { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614 }, + { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316 }, + { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322 }, + { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055 }, + { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291 }, + { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374 }, + { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896 }, + { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738 }, + { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568 }, + { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653 }, + { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] + +[[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 }, +] diff --git a/.conflict-files b/.conflict-files new file mode 100644 index 0000000..22d1b75 --- /dev/null +++ b/.conflict-files @@ -0,0 +1,3 @@ +ancestorEntries = ["grelmicro/sync/postgres.py"] +ourEntries = ["grelmicro/sync/postgres.py"] +theirEntries = ["grelmicro/sync/postgres.py"] diff --git a/.conflict-side-0/.github/workflows/ci.yml b/.conflict-side-0/.github/workflows/ci.yml new file mode 100644 index 0000000..5fb99bb --- /dev/null +++ b/.conflict-side-0/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras + + - name: Run Mypy + run: uv run mypy . + + test: + name: Test Python ${{ matrix.python }} + runs-on: "ubuntu-latest" + strategy: + fail-fast: true + matrix: + python: ["3.11", "3.12", "3.13"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras --python ${{ matrix.python }} + + - name: Run unit tests + run: uv run pytest -x + + - name: Run integration tests + run: uv run pytest -x -m integration --cov-append + + - name: Rename coverage report + run: mv .coverage .coverage.py${{ matrix.python }} + + - name: Save coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.python }} + path: .coverage.py${{ matrix.python }} + include-hidden-files: true + + coverage-report: + name: Coverage report + runs-on: ubuntu-latest + needs: [test] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get coverage reports + uses: actions/download-artifact@v4 + with: + pattern: coverage-* + merge-multiple: true + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras + + - name: Combine coverage reports + run: | + uv run coverage combine .coverage.* + uv run coverage xml -o cov.xml + + - name: Upload coverage report to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./cov.xml diff --git a/.conflict-side-0/.github/workflows/release.yml b/.conflict-side-0/.github/workflows/release.yml new file mode 100644 index 0000000..c8d4bab --- /dev/null +++ b/.conflict-side-0/.github/workflows/release.yml @@ -0,0 +1,110 @@ +name: Release + +on: + release: + types: + - published + +jobs: + bump-version: + name: Bump version + runs-on: ubuntu-latest + steps: + + - name: Generate GitHub App Token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.GRELINFO_ID }} + private-key: ${{ secrets.GRELINFO_KEY }} + + - name: Get GitHub App User ID + id: user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + - name: Configure Git App Credentials + run: | + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}@users.noreply.github.com>' + + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Get release version + id: release-version + run: echo "release-version=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" + + - name: Get current version + id: current-version + run: echo "current-version=$(uv run hatch version)" >> "$GITHUB_OUTPUT" + + - name: Bump version if necessary + if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} + run: | + uv run hatch version $RELEASE_VERSION + uv lock + + - name: Commit and push changes + if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} + run: | + git add . + git commit -m "🚀 Release $RELEASE_VERSION" + git tag -f $RELEASE_VERSION + git push origin $RELEASE_VERSION --force + git push origin HEAD:main + + publish-docs: + runs-on: ubuntu-latest + needs: [bump-version] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Configure Git Credentials + run: | + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --group docs + + - name: Deploy docs on GitHub Pages + run: uv run mkdocs gh-deploy --force + + publish-pypi: + needs: [bump-version] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Build + run: uv build + + - name: Publish + run: uv publish -t ${{ secrets.PYPI_TOKEN }} diff --git a/.conflict-side-0/.gitignore b/.conflict-side-0/.gitignore new file mode 100644 index 0000000..0d118ab --- /dev/null +++ b/.conflict-side-0/.gitignore @@ -0,0 +1,17 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Coverage +cov.xml +.coverage + +# Mkdocs +site/ diff --git a/.conflict-side-0/.pre-commit-config.yaml b/.conflict-side-0/.pre-commit-config.yaml new file mode 100644 index 0000000..5e5a141 --- /dev/null +++ b/.conflict-side-0/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +default_language_version: + python: python3.11 + +repos: + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + - id: check-added-large-files + - id: trailing-whitespace + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.0 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + +- repo: local + hooks: + + - id: readme-to-docs + name: readme-to-docs + description: "Copy README.md to docs/index.md" + entry: cp README.md docs/index.md + language: system + pass_filenames: false + + # --- Local development hooks --- + - id: uv-lock + name: uv-lock + description: "Lock dependencies with 'uv lock'" + entry: uv lock + language: system + pass_filenames: false + + - id: mypy + name: mypy + description: "Run 'mypy' for static type checking" + entry: uv run mypy + language: system + types: [python] + require_serial: true + + - id: pytest + name: pytest + description: "Run 'pytest' for unit testing" + entry: uv run pytest --cov-fail-under=90 + language: system + pass_filenames: false + +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate + skip: [uv-lock, mypy, pytest] diff --git a/.conflict-side-0/.vscode/settings.json b/.conflict-side-0/.vscode/settings.json new file mode 100644 index 0000000..806ffc4 --- /dev/null +++ b/.conflict-side-0/.vscode/settings.json @@ -0,0 +1,58 @@ +{ + // Editor settings + "editor.rulers": [80, 100], + "files.trimTrailingWhitespace": true, + "terminal.integrated.scrollback": 10000, + + // Files exclude settings + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/__pycache__": true, + "**/.venv": true, + "**/.mypy_cache": true, + "**/.pytest_cache": true, + "**/.ruff_cache": true, + ".coverage": true + }, + + // Python settings + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.activateEnvironment": true, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": ["--no-cov", "--color=yes"], + "python.analysis.inlayHints.pytestParameters": true, + + // Python editor settings + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + + // Mypy settings + "mypy-type-checker.importStrategy": "fromEnvironment", + + // YAML settings + "yaml.schemas": { + "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + }, + "yaml.customTags": [ + "!ENV scalar", + "!ENV sequence", + "!relative scalar", + "tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg", + "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", + "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", + "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" + ], + + // Ruff settings + "ruff.configurationPreference": "filesystemFirst" +} diff --git a/.conflict-side-0/LICENSE b/.conflict-side-0/LICENSE new file mode 100644 index 0000000..18dafa2 --- /dev/null +++ b/.conflict-side-0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Loïc Gremaud + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.conflict-side-0/README.md b/.conflict-side-0/README.md new file mode 100644 index 0000000..9f3e0ff --- /dev/null +++ b/.conflict-side-0/README.md @@ -0,0 +1,158 @@ +# Grelmicro + +Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. + +It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. + +[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) +[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) + +______________________________________________________________________ + +**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) + +**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) + +______________________________________________________________________ + +## Overview + +Grelmicro provides essential features for building robust distributed systems, including: + +- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. +- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. +- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. +- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. + +These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. + +### Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. + +### Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. + +The available primitives are: + +- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **Lock**: A distributed lock that can be used to synchronize access to shared resources. + +### Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Installation + +```bash +pip install grelmicro +``` + +## Examples + +### FastAPI Integration + +- Create a file `main.py` with: + +```python +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") +``` + +## Dependencies + +Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. + +### `standard` Dependencies + +When you install Grelmicro with `pip install grelmicro[standard]` it comes with: + +- `loguru`: A Python logging library. +- `orjson`: A fast, correct JSON library for Python. + +### `redis` Dependencies + +When you install Grelmicro with `pip install grelmicro[redis]` it comes with: + +- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). + +### `postgres` Dependencies + +When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: + +- `asyncpg`: The Python `asyncio` interface for PostgreSQL. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/.conflict-side-0/docs/index.md b/.conflict-side-0/docs/index.md new file mode 100644 index 0000000..9f3e0ff --- /dev/null +++ b/.conflict-side-0/docs/index.md @@ -0,0 +1,158 @@ +# Grelmicro + +Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. + +It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. + +[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) +[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) + +______________________________________________________________________ + +**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) + +**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) + +______________________________________________________________________ + +## Overview + +Grelmicro provides essential features for building robust distributed systems, including: + +- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. +- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. +- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. +- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. + +These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. + +### Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. + +### Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. + +The available primitives are: + +- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **Lock**: A distributed lock that can be used to synchronize access to shared resources. + +### Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Installation + +```bash +pip install grelmicro +``` + +## Examples + +### FastAPI Integration + +- Create a file `main.py` with: + +```python +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") +``` + +## Dependencies + +Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. + +### `standard` Dependencies + +When you install Grelmicro with `pip install grelmicro[standard]` it comes with: + +- `loguru`: A Python logging library. +- `orjson`: A fast, correct JSON library for Python. + +### `redis` Dependencies + +When you install Grelmicro with `pip install grelmicro[redis]` it comes with: + +- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). + +### `postgres` Dependencies + +When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: + +- `asyncpg`: The Python `asyncio` interface for PostgreSQL. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/.conflict-side-0/docs/logging.md b/.conflict-side-0/docs/logging.md new file mode 100644 index 0000000..4575b03 --- /dev/null +++ b/.conflict-side-0/docs/logging.md @@ -0,0 +1,73 @@ +# Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to stdout. It supports JSON formatting and allows log level configuration via environment variables. + +## Dependencies + +For the moment the `logging` package is only working with the `loguru` Python logging library. +When `orjson` is installed, it will be used as the default JSON serializer for faster performance, otherwise, the standard `json` library will be used. + +[**Loguru**](https://loguru.readthedocs.io/en/stable/overview.html) is used as the logging library. + +For using `logging` package, please install the required dependencies: + +=== "Standard" + ```bash + pip install grelmicro[standard] + ``` + +=== "only loguru (minimum)" + ```bash + pip install loguru + ``` + +=== "loguru and orjson (manual)" + ```bash + pip install loguru orjson + ``` + + +## Configure Logging + +Just call the `configure_logging` function to set up the logging system. + +```python +{!> ../examples/logging/configure_logging.py!} +``` + +### Settings + +You can change the default settings using the following environment variables: + +- `LOG_LEVEL`: Set the desired log level (default: `INFO`). +- `LOG_FORMAT`: Choose the log format. Options are `TEXT` and `JSON`, or you can provide a custom [loguru](https://loguru.readthedocs.io/en/stable/overview.html) template (default: `TEXT`). + + +## Examples + +### Basic Usage + +Here is a quick example of how to use the logging system: + +```python +{!> ../examples/logging/basic.py!} +``` + +The console output, `stdout` will be: + +```json +{!> ../examples/logging/basic.log!} +``` + +### FastAPI Integration + +You can use the logging system with FastAPI as well: + +```python +{!> ../examples/logging/fastapi.py!} +``` + +!!! warning + It is crucial to call `configure_logging` during the lifespan of the FastAPI application. Failing to do so may result in the FastAPI CLI resetting the logging configuration. diff --git a/.conflict-side-0/docs/sync.md b/.conflict-side-0/docs/sync.md new file mode 100644 index 0000000..4c3b881 --- /dev/null +++ b/.conflict-side-0/docs/sync.md @@ -0,0 +1,81 @@ +# Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends (see more in the Backends section). + +The available primitives are: + +- **[Leader Election](#leader-election)**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **[Lock](#lock)**: A distributed lock that can be used to synchronize access to shared resources. + +The synchronization primitives can be used in combination with the `TaskManager` and `TaskRouter` to control task execution in a distributed system (see more in [Task Scheduler](task.md)). + +## Backend + +You must load a synchronization backend before using synchronization primitives. + +!!! note + Although Grelmicro use AnyIO for concurrency, the backends generally depend on `asyncio`, therefore Trio is not supported. + +You can initialize a backend like this: + +=== "Redis" + ```python + {!> ../examples/sync/redis.py!} + ``` + +=== "Postgres" + ```python + {!> ../examples/sync/postgres.py!} + ``` + +=== "Memory (For Testing Only)" + ```python + {!> ../examples/sync/memory.py!} + ``` + +!!! warning + Please make sure to use a proper way to store connection url, such as environment variables (not like the example above). + +!!! tip + Feel free to create your own backend and contribute it. In the `sync.abc` module, you can find the protocol for creating new backends. + + + +## Leader Election + +Leader election ensures that only one worker in the cluster is designated as the leader at any given time using a distributed lock. + +The leader election service is responsible for acquiring and renewing the distributed lock. It runs as an AnyIO Task that can be easily started with the [Task Manager](./task.md#task-manager). This service operates in the background, automatically renewing the lock to prevent other workers from acquiring it. The lock is released automatically when the task is cancelled or during shutdown. + +=== "Task Manager (Recommended)" + ```python + {!> ../examples/sync/leaderelection_task.py!} + ``` + +=== "AnyIO Task Group (Advanced)" + ```python + {!> ../examples/sync/leaderelection_anyio.py!} + ``` + +## Lock + +The lock is a distributed lock that can be used to synchronize access to shared resources. + +The lock supports the following features: + +- **Async**: The lock must be acquired and released asynchronously. +- **Distributed**: The lock must be distributed across multiple workers. +- **Reentrant**: The lock must allow the same token to acquire it multiple times to extend the lease. +- **Expiring**: The lock must have a timeout to auto-release after an interval to prevent deadlocks. +- **Non-blocking**: Lock operations must not block the async event loop. +- **Vendor-agnostic**: Must support multiple backends (Redis, Postgres, ConfigMap, etc.). + + +```python +{!> ../examples/sync/lock.py!} +``` + +!!! warning + The lock is designed for use within an async event loop and is not thread-safe or process-safe. diff --git a/.conflict-side-0/docs/task.md b/.conflict-side-0/docs/task.md new file mode 100644 index 0000000..b6f0e00 --- /dev/null +++ b/.conflict-side-0/docs/task.md @@ -0,0 +1,85 @@ +# Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Task Manager + +The `TaskManager` class is the main entry point to manage scheduled tasks. You need to start the task manager to run the scheduled tasks using the application lifespan. + +=== "FastAPI" + + ```python + {!> ../examples/task/fastapi.py!} + ``` + +=== "FastStream" + + ```python + + {!> ../examples/task/faststream.py!} + ``` + +## Interval Task + +To create an `IntervalTask`, use the `interval` decorator method of the `TaskManager` instance. This decorator allows tasks to run at specified intervals. + +> **Note**: The interval specifies the waiting time between task executions. Ensure that the task execution duration is considered to meet deadlines effectively. + +=== "TaskManager" + + ```python + {!> ../examples/task/interval_manager.py!} + ``` + +=== "TaskRouter" + + ```python + {!> ../examples/task/interval_router.py!} + ``` + + +## Synchronization + +The Task can be synchronized using a [Synchoronization Primitive](sync.md) to control concurrency and manage simultaneous task execution. + +=== "Lock" + + ```python + {!> ../examples/task/lock.py!} + ``` + + +=== "Leader Election" + + + ```python + {!> ../examples/task/leaderelection.py!} + ``` + +## Task Router + +For bigger applications, you can use the `TaskRouter` class to manage tasks in different modules. + + +```python +{!> ../examples/task/router.py [ln:1-10]!} +``` + +Then you can include the `TaskRouter` into the `TaskManager` or other routers using the `include_router` method. + +```python +{!> ../examples/task/router.py [ln:12-]!} +``` + +!!! tip + The `TaskRouter` follows the same philosophy as the `APIRouter` in FastAPI or the **Router** in FastStream. diff --git a/.conflict-side-0/examples/__init__.py b/.conflict-side-0/examples/__init__.py new file mode 100644 index 0000000..73b7d32 --- /dev/null +++ b/.conflict-side-0/examples/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Examples.""" diff --git a/.conflict-side-0/examples/logging/__init__.py b/.conflict-side-0/examples/logging/__init__.py new file mode 100644 index 0000000..bf04afe --- /dev/null +++ b/.conflict-side-0/examples/logging/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Logging Examples.""" diff --git a/.conflict-side-0/examples/logging/basic.log b/.conflict-side-0/examples/logging/basic.log new file mode 100644 index 0000000..33c8e37 --- /dev/null +++ b/.conflict-side-0/examples/logging/basic.log @@ -0,0 +1,4 @@ +{"time":"2024-11-25T15:56:36.066922+01:00","level":"INFO","thread":"MainThread","logger":"__main__::7","msg":"This is an info message"} +{"time":"2024-11-25T15:56:36.067063+01:00","level":"WARNING","thread":"MainThread","logger":"__main__::8","msg":"This is a warning message with context","ctx":{"user":"Alice"}} +{"time":"2024-11-25T15:56:36.067105+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::9","msg":"This is an error message with context","ctx":{"user":"Bob"}} +{"time":"2024-11-25T15:56:36.067134+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::14","msg":"This is an exception message with context","ctx":{"user":"Charlie","exception":"ValueError: This is an exception"}} diff --git a/.conflict-side-0/examples/logging/basic.py b/.conflict-side-0/examples/logging/basic.py new file mode 100644 index 0000000..889f160 --- /dev/null +++ b/.conflict-side-0/examples/logging/basic.py @@ -0,0 +1,17 @@ +from loguru import logger + +from grelmicro.logging import configure_logging + +configure_logging() + +logger.debug("This is a debug message") +logger.info("This is an info message") +logger.warning("This is a warning message with context", user="Alice") +logger.error("This is an error message with context", user="Bob") + +try: + raise ValueError("This is an exception message") +except ValueError: + logger.exception( + "This is an exception message with context", user="Charlie" + ) diff --git a/.conflict-side-0/examples/logging/configure_logging.py b/.conflict-side-0/examples/logging/configure_logging.py new file mode 100644 index 0000000..0ffacd8 --- /dev/null +++ b/.conflict-side-0/examples/logging/configure_logging.py @@ -0,0 +1,3 @@ +from grelmicro.logging import configure_logging + +configure_logging() diff --git a/.conflict-side-0/examples/logging/fastapi.py b/.conflict-side-0/examples/logging/fastapi.py new file mode 100644 index 0000000..7f318c5 --- /dev/null +++ b/.conflict-side-0/examples/logging/fastapi.py @@ -0,0 +1,22 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from loguru import logger + +from grelmicro.logging import configure_logging + + +@asynccontextmanager +def lifespan_startup(): + # Ensure logging is configured during startup + configure_logging() + yield + + +app = FastAPI() + + +@app.get("/") +def root(): + logger.info("This is an info message") + return {"Hello": "World"} diff --git a/.conflict-side-0/examples/simple_fastapi_app.py b/.conflict-side-0/examples/simple_fastapi_app.py new file mode 100644 index 0000000..ff52251 --- /dev/null +++ b/.conflict-side-0/examples/simple_fastapi_app.py @@ -0,0 +1,54 @@ +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") diff --git a/.conflict-side-0/examples/single_file_app.py b/.conflict-side-0/examples/single_file_app.py new file mode 100644 index 0000000..4f4bb87 --- /dev/null +++ b/.conflict-side-0/examples/single_file_app.py @@ -0,0 +1,114 @@ +import time +from contextlib import asynccontextmanager +from typing import Annotated + +import anyio +import typer +from fast_depends import Depends +from fastapi import FastAPI + +from grelmicro.sync.leaderelection import LeaderElection +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.task import TaskManager + +backend = MemorySyncBackend() +task = TaskManager() + + +@asynccontextmanager +async def lifespan(app): + async with backend, task: + typer.echo("App started") + yield + typer.echo("App stopped") + + +app = FastAPI(lifespan=lifespan) + +leased_lock_10sec = Lock( + name="leased_lock_10sec", + lease_duration=10, + backend=backend, +) +leased_lock_5sec = Lock( + name="leased_lock_5sec", + lease_duration=5, + backend=backend, +) + +leader_election = LeaderElection(name="simple-leader", backend=backend) + +task.add_task(leader_election) + + +@task.interval(seconds=1) +def sync_func_with_no_param(): + typer.echo("sync_with_no_param") + + +@task.interval(seconds=2) +async def async_func_with_no_param(): + typer.echo("async_with_no_param") + + +def sync_dependency(): + return "sync_dependency" + + +@task.interval(seconds=3) +def sync_func_with_sync_dependency( + sync_dependency: Annotated[str, Depends(sync_dependency)], +): + typer.echo(sync_dependency) + + +async def async_dependency(): + yield "async_with_async_dependency" + + +@task.interval(seconds=4) +async def async_func_with_async_dependency( + async_dependency: Annotated[str, Depends(async_dependency)], +): + typer.echo(async_dependency) + + +@task.interval(seconds=15, sync=leased_lock_10sec) +def sync_func_with_leased_lock_10sec(): + typer.echo("sync_func_with_leased_lock_10sec") + time.sleep(9) + + +@task.interval(seconds=15, sync=leased_lock_10sec) +async def async_func_with_leased_lock_10sec(): + typer.echo("async_func_with_leased_lock_10sec") + await anyio.sleep(9) + + +@task.interval(seconds=15, sync=leased_lock_5sec) +def sync_func_with_sync_dependency_and_leased_lock_5sec( + sync_dependency: Annotated[str, Depends(sync_dependency)], +): + typer.echo(sync_dependency) + time.sleep(4) + + +@task.interval(seconds=15, sync=leased_lock_5sec) +async def async_func_with_async_dependency_and_leased_lock_5sec( + async_dependency: Annotated[str, Depends(async_dependency)], +): + typer.echo(async_dependency) + await anyio.sleep(4) + + +@task.interval(seconds=15, sync=leader_election) +def sync_func_with_leader_election(): + typer.echo("sync_func_with_leader_election") + time.sleep(30) + + +@task.interval(seconds=15, sync=leader_election) +async def async_func_with_leader_election(): + typer.echo("async_func_with_leader_election") + await anyio.sleep(30) diff --git a/.conflict-side-0/examples/sync/__init__.py b/.conflict-side-0/examples/sync/__init__.py new file mode 100644 index 0000000..acd409a --- /dev/null +++ b/.conflict-side-0/examples/sync/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Synchronization Primitives Examples.""" diff --git a/.conflict-side-0/examples/sync/leaderelection_anyio.py b/.conflict-side-0/examples/sync/leaderelection_anyio.py new file mode 100644 index 0000000..784f188 --- /dev/null +++ b/.conflict-side-0/examples/sync/leaderelection_anyio.py @@ -0,0 +1,11 @@ +from anyio import create_task_group, sleep_forever + +from grelmicro.sync.leaderelection import LeaderElection + +leader = LeaderElection("cluster_group") + + +async def main(): + async with create_task_group() as tg: + await tg.start(leader) + await sleep_forever() diff --git a/.conflict-side-0/examples/sync/leaderelection_task.py b/.conflict-side-0/examples/sync/leaderelection_task.py new file mode 100644 index 0000000..58fa926 --- /dev/null +++ b/.conflict-side-0/examples/sync/leaderelection_task.py @@ -0,0 +1,6 @@ +from grelmicro.sync import LeaderElection +from grelmicro.task import TaskManager + +leader = LeaderElection("cluster_group") +task = TaskManager() +task.add_task(leader) diff --git a/.conflict-side-0/examples/sync/lock.py b/.conflict-side-0/examples/sync/lock.py new file mode 100644 index 0000000..7f38fe6 --- /dev/null +++ b/.conflict-side-0/examples/sync/lock.py @@ -0,0 +1,8 @@ +from grelmicro.sync import Lock + +lock = Lock("resource_name") + + +async def main(): + async with lock: + print("Protected resource accessed") diff --git a/.conflict-side-0/examples/sync/memory.py b/.conflict-side-0/examples/sync/memory.py new file mode 100644 index 0000000..7eefea9 --- /dev/null +++ b/.conflict-side-0/examples/sync/memory.py @@ -0,0 +1,3 @@ +from grelmicro.sync.memory import MemorySyncBackend + +backend = MemorySyncBackend() diff --git a/.conflict-side-0/examples/sync/postgres.py b/.conflict-side-0/examples/sync/postgres.py new file mode 100644 index 0000000..ea8b8c3 --- /dev/null +++ b/.conflict-side-0/examples/sync/postgres.py @@ -0,0 +1,3 @@ +from grelmicro.sync.postgres import PostgresSyncBackend + +backend = PostgresSyncBackend("postgresql://user:password@localhost:5432/db") diff --git a/.conflict-side-0/examples/sync/redis.py b/.conflict-side-0/examples/sync/redis.py new file mode 100644 index 0000000..0625f5d --- /dev/null +++ b/.conflict-side-0/examples/sync/redis.py @@ -0,0 +1,3 @@ +from grelmicro.sync.redis import RedisSyncBackend + +backend = RedisSyncBackend("redis://localhost:6379/0") diff --git a/.conflict-side-0/examples/task/__init__.py b/.conflict-side-0/examples/task/__init__.py new file mode 100644 index 0000000..20f7752 --- /dev/null +++ b/.conflict-side-0/examples/task/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Task Scheduler Examples.""" diff --git a/.conflict-side-0/examples/task/fastapi.py b/.conflict-side-0/examples/task/fastapi.py new file mode 100644 index 0000000..16aaa8e --- /dev/null +++ b/.conflict-side-0/examples/task/fastapi.py @@ -0,0 +1,16 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from grelmicro.task import TaskManager + +task = TaskManager() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + async with task: + yield + + +app = FastAPI(lifespan=lifespan) diff --git a/.conflict-side-0/examples/task/faststream.py b/.conflict-side-0/examples/task/faststream.py new file mode 100644 index 0000000..688c8d9 --- /dev/null +++ b/.conflict-side-0/examples/task/faststream.py @@ -0,0 +1,18 @@ +from contextlib import asynccontextmanager + +from faststream import ContextRepo, FastStream +from faststream.redis import RedisBroker + +from grelmicro.task import TaskManager + +task = TaskManager() + + +@asynccontextmanager +async def lifespan(context: ContextRepo): + async with task: + yield + + +broker = RedisBroker() +app = FastStream(broker, lifespan=lifespan) diff --git a/.conflict-side-0/examples/task/interval_manager.py b/.conflict-side-0/examples/task/interval_manager.py new file mode 100644 index 0000000..91beb2e --- /dev/null +++ b/.conflict-side-0/examples/task/interval_manager.py @@ -0,0 +1,8 @@ +from grelmicro.task import TaskManager + +task = TaskManager() + + +@task.interval(seconds=5) +async def my_task(): + print("Hello, World!") diff --git a/.conflict-side-0/examples/task/interval_router.py b/.conflict-side-0/examples/task/interval_router.py new file mode 100644 index 0000000..f114ad7 --- /dev/null +++ b/.conflict-side-0/examples/task/interval_router.py @@ -0,0 +1,8 @@ +from grelmicro.task import TaskRouter + +task = TaskRouter() + + +@task.interval(seconds=5) +async def my_task(): + print("Hello, World!") diff --git a/.conflict-side-0/examples/task/leaderelection.py b/.conflict-side-0/examples/task/leaderelection.py new file mode 100644 index 0000000..ad12773 --- /dev/null +++ b/.conflict-side-0/examples/task/leaderelection.py @@ -0,0 +1,12 @@ +from grelmicro.sync import LeaderElection +from grelmicro.task import TaskManager + +leader = LeaderElection("my_task") +task = TaskManager() +task.add_task(leader) + + +@task.interval(seconds=5, sync=leader) +async def my_task(): + async with leader: + print("Hello, World!") diff --git a/.conflict-side-0/examples/task/lock.py b/.conflict-side-0/examples/task/lock.py new file mode 100644 index 0000000..cdbf795 --- /dev/null +++ b/.conflict-side-0/examples/task/lock.py @@ -0,0 +1,11 @@ +from grelmicro.sync import Lock +from grelmicro.task import TaskManager + +lock = Lock("my_task") +task = TaskManager() + + +@task.interval(seconds=5, sync=lock) +async def my_task(): + async with lock: + print("Hello, World!") diff --git a/.conflict-side-0/examples/task/router.py b/.conflict-side-0/examples/task/router.py new file mode 100644 index 0000000..2b166aa --- /dev/null +++ b/.conflict-side-0/examples/task/router.py @@ -0,0 +1,15 @@ +from grelmicro.task import TaskRouter + + +router = TaskRouter() + + +@router.interval(seconds=5) +async def my_task(): + print("Hello, World!") + + +from grelmicro.task.manager import TaskManager + +task = TaskManager() +task.include_router(router) diff --git a/.conflict-side-0/grelmicro/__init__.py b/.conflict-side-0/grelmicro/__init__.py new file mode 100644 index 0000000..7cc6d82 --- /dev/null +++ b/.conflict-side-0/grelmicro/__init__.py @@ -0,0 +1,3 @@ +"""Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python.""" # noqa: E501 + +__version__ = "0.2.2" diff --git a/.conflict-side-0/grelmicro/errors.py b/.conflict-side-0/grelmicro/errors.py new file mode 100644 index 0000000..141f82e --- /dev/null +++ b/.conflict-side-0/grelmicro/errors.py @@ -0,0 +1,52 @@ +"""Grelmicro Errors.""" + +from typing import assert_never + +from pydantic import ValidationError + + +class GrelmicroError(Exception): + """Base Grelmicro error.""" + + +class OutOfContextError(GrelmicroError, RuntimeError): + """Outside Context Error. + + Raised when a method is called outside of the context manager. + """ + + def __init__(self, cls: object, method_name: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not call {cls.__class__.__name__}.{method_name} outside of the context manager" + ) + + +class DependencyNotFoundError(GrelmicroError, ImportError): + """Dependency Not Found Error.""" + + def __init__(self, *, module: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not import module {module}, try running 'pip install {module}'" + ) + + +class SettingsValidationError(GrelmicroError, ValueError): + """Settings Validation Error.""" + + def __init__(self, error: ValidationError | str) -> None: + """Initialize the error.""" + if isinstance(error, str): + details = error + elif isinstance(error, ValidationError): + details = "\n".join( + f"- {data['loc'][0]}: {data['msg']} [input={data['input']}]" + for data in error.errors() + ) + else: + assert_never(error) + + super().__init__( + f"Could not validate environment variables settings:\n{details}" + ) diff --git a/.conflict-side-0/grelmicro/logging/__init__.py b/.conflict-side-0/grelmicro/logging/__init__.py new file mode 100644 index 0000000..60d3d45 --- /dev/null +++ b/.conflict-side-0/grelmicro/logging/__init__.py @@ -0,0 +1,5 @@ +"""Grelmicro Logging.""" + +from grelmicro.logging.loguru import configure_logging + +__all__ = ["configure_logging"] diff --git a/.conflict-side-0/grelmicro/logging/config.py b/.conflict-side-0/grelmicro/logging/config.py new file mode 100644 index 0000000..a6301c1 --- /dev/null +++ b/.conflict-side-0/grelmicro/logging/config.py @@ -0,0 +1,43 @@ +"""Logging Configuration.""" + +from enum import StrEnum +from typing import Self + +from pydantic import Field +from pydantic_settings import BaseSettings + + +class _CaseInsensitiveEnum(StrEnum): + @classmethod + def _missing_(cls, value: object) -> Self | None: + value = str(value).lower() + for member in cls: + if member.lower() == value: + return member + return None + + +class LoggingLevelType(_CaseInsensitiveEnum): + """Logging Level Enum.""" + + DEBUG = "DEBUG" + INFO = "INFO" + WARNING = "WARNING" + ERROR = "ERROR" + CRITICAL = "CRITICAL" + + +class LoggingFormatType(_CaseInsensitiveEnum): + """Logging Format Enum.""" + + JSON = "JSON" + TEXT = "TEXT" + + +class LoggingSettings(BaseSettings): + """Logging Settings.""" + + LOG_LEVEL: LoggingLevelType = LoggingLevelType.INFO + LOG_FORMAT: LoggingFormatType | str = Field( + LoggingFormatType.JSON, union_mode="left_to_right" + ) diff --git a/.conflict-side-0/grelmicro/logging/errors.py b/.conflict-side-0/grelmicro/logging/errors.py new file mode 100644 index 0000000..097006f --- /dev/null +++ b/.conflict-side-0/grelmicro/logging/errors.py @@ -0,0 +1,7 @@ +"""Grelmicro Logging Errors.""" + +from grelmicro.errors import SettingsValidationError + + +class LoggingSettingsValidationError(SettingsValidationError): + """Logging Settings Validation Error.""" diff --git a/.conflict-side-0/grelmicro/logging/loguru.py b/.conflict-side-0/grelmicro/logging/loguru.py new file mode 100644 index 0000000..a94202c --- /dev/null +++ b/.conflict-side-0/grelmicro/logging/loguru.py @@ -0,0 +1,121 @@ +"""Loguru Logging.""" + +import json +import sys +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, NotRequired + +from pydantic import ValidationError +from typing_extensions import TypedDict + +from grelmicro.errors import DependencyNotFoundError +from grelmicro.logging.config import LoggingFormatType, LoggingSettings +from grelmicro.logging.errors import LoggingSettingsValidationError + +if TYPE_CHECKING: + from loguru import FormatFunction, Record + +try: + import loguru +except ImportError: # pragma: no cover + loguru = None # type: ignore[assignment] + +try: + import orjson + + def _json_dumps(obj: Mapping[str, Any]) -> str: + return orjson.dumps(obj).decode("utf-8") +except ImportError: # pragma: no cover + import json + + _json_dumps = json.dumps + + +JSON_FORMAT = "{extra[serialized]}" +TEXT_FORMAT = ( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + "{name}:{function}:{line} - {message}" +) + + +class JSONRecordDict(TypedDict): + """JSON log record representation. + + The time use a ISO 8601 string. + """ + + time: str + level: str + msg: str + logger: str | None + thread: str + ctx: NotRequired[dict[Any, Any]] + + +def json_patcher(record: "Record") -> None: + """Patch the serialized log record with `JSONRecordDict` representation.""" + json_record = JSONRecordDict( + time=record["time"].isoformat(), + level=record["level"].name, + thread=record["thread"].name, + logger=f'{record["name"]}:{record["function"]}:{record["line"]}', + msg=record["message"], + ) + + ctx = {k: v for k, v in record["extra"].items() if k != "serialized"} + exception = record["exception"] + + if exception and exception.type: + ctx["exception"] = f"{exception.type.__name__}: {exception.value!s}" + + if ctx: + json_record["ctx"] = ctx + + record["extra"]["serialized"] = _json_dumps(json_record) + + +def json_formatter(record: "Record") -> str: + """Format log record with `JSONRecordDict` representation. + + This function does not return the formatted record directly but provides the format to use when + writing to the sink. + """ + json_patcher(record) + return JSON_FORMAT + "\n" + + +def configure_logging() -> None: + """Configure logging with loguru. + + Simple twelve-factor app logging configuration that logs to stdout. + + The following environment variables are used: + - LOG_LEVEL: The log level to use (default: INFO). + - LOG_FORMAT: JSON | TEXT or any loguru template to format logged message (default: JSON). + + Raises: + MissingDependencyError: If the loguru module is not installed. + LoggingSettingsError: If the LOG_FORMAT or LOG_LEVEL environment variable is invalid + """ + if not loguru: + raise DependencyNotFoundError(module="loguru") + + try: + settings = LoggingSettings() + except ValidationError as error: + raise LoggingSettingsValidationError(error) from None + + logger = loguru.logger + log_format: str | FormatFunction = settings.LOG_FORMAT + + if log_format is LoggingFormatType.JSON: + log_format = json_formatter + elif log_format is LoggingFormatType.TEXT: + log_format = TEXT_FORMAT + + logger.remove() + logger.add( + sys.stdout, + level=settings.LOG_LEVEL, + format=log_format, + ) diff --git a/.conflict-side-0/grelmicro/py.typed b/.conflict-side-0/grelmicro/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/.conflict-side-0/grelmicro/sync/__init__.py b/.conflict-side-0/grelmicro/sync/__init__.py new file mode 100644 index 0000000..128d56c --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/__init__.py @@ -0,0 +1,6 @@ +"""Grelmicro Synchronization Primitives.""" + +from grelmicro.sync.leaderelection import LeaderElection +from grelmicro.sync.lock import Lock + +__all__ = ["LeaderElection", "Lock"] diff --git a/.conflict-side-0/grelmicro/sync/_backends.py b/.conflict-side-0/grelmicro/sync/_backends.py new file mode 100644 index 0000000..66f4b9f --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/_backends.py @@ -0,0 +1,30 @@ +"""Grelmicro Backend Registry. + +Contains loaded backends of each type to be used as default. + +Note: + For now, only lock backends are supported, but other backends may be added in the future. +""" + +from typing import Literal, NotRequired, TypedDict + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import BackendNotLoadedError + + +class LoadedBackendsDict(TypedDict): + """Loaded backends type.""" + + lock: NotRequired[SyncBackend] + + +loaded_backends: LoadedBackendsDict = {} + + +def get_sync_backend() -> SyncBackend: + """Get the lock backend.""" + backend: Literal["lock"] = "lock" + try: + return loaded_backends[backend] + except KeyError: + raise BackendNotLoadedError(backend) from None diff --git a/.conflict-side-0/grelmicro/sync/_base.py b/.conflict-side-0/grelmicro/sync/_base.py new file mode 100644 index 0000000..a0e6fb0 --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/_base.py @@ -0,0 +1,101 @@ +"""Grelmicro Lock API.""" + +from types import TracebackType +from typing import Annotated, Protocol, Self +from uuid import UUID + +from pydantic import BaseModel, ConfigDict +from typing_extensions import Doc + +from grelmicro.sync.abc import Synchronization + + +class BaseLockConfig(BaseModel): + """Base Lock Config.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + name: Annotated[ + str, + Doc(""" + The name of the resource to lock. + """), + ] + worker: Annotated[ + str | UUID, + Doc(""" + The worker identity. + + By default, use a UUIDv1. + """), + ] + + +class BaseLock(Synchronization, Protocol): + """Base Lock Protocol.""" + + async def __aenter__(self) -> Self: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + """ + ... + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + ... + + @property + def config(self) -> BaseLockConfig: + """Return the config.""" + ... + + async def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + ... + + async def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + WouldBlock: If the lock cannot be acquired without blocking. + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + ... + + async def release(self) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + ... + + async def locked(self) -> bool: + """Check if the lock is currently held.""" + ... + + async def owned(self) -> bool: + """Check if the lock is currently held by the current token.""" + ... diff --git a/.conflict-side-0/grelmicro/sync/_utils.py b/.conflict-side-0/grelmicro/sync/_utils.py new file mode 100644 index 0000000..2ad5dda --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/_utils.py @@ -0,0 +1,38 @@ +from threading import get_ident +from uuid import NAMESPACE_DNS, UUID, uuid3 + +from anyio import get_current_task + + +def generate_worker_namespace(worker: str) -> UUID: + """Generate a worker UUIDv3 namespace. + + Generate a worker UUID using UUIDv3 with the DNS namespace. + """ + return uuid3(namespace=NAMESPACE_DNS, name=worker) + + +def generate_task_token(worker: UUID | str) -> str: + """Generate a task UUID. + + The worker namespace is generated using `generate_worker_uuid` if the worker is a string. + Generate a task UUID using UUIDv3 with the worker namespace and the async task ID. + """ + worker = ( + generate_worker_namespace(worker) if isinstance(worker, str) else worker + ) + task = str(get_current_task().id) + return str(uuid3(namespace=worker, name=task)) + + +def generate_thread_token(worker: UUID | str) -> str: + """Generate a thread UUID. + + The worker namespace is generated using `generate_worker_uuid` if the worker is a string. + Generate a thread UUID using UUIDv3 with the worker namespace and the current thread ID. + """ + worker = ( + generate_worker_namespace(worker) if isinstance(worker, str) else worker + ) + thread = str(get_ident()) + return str(uuid3(namespace=worker, name=thread)) diff --git a/.conflict-side-0/grelmicro/sync/abc.py b/.conflict-side-0/grelmicro/sync/abc.py new file mode 100644 index 0000000..507477c --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/abc.py @@ -0,0 +1,106 @@ +"""Grelmicro Synchronization Abstract Base Classes and Protocols.""" + +from types import TracebackType +from typing import Protocol, Self, runtime_checkable + +from pydantic import PositiveFloat + + +class SyncBackend(Protocol): + """Synchronization Backend Protocol. + + This is the low level API for the distributed lock backend that is platform agnostic. + """ + + async def __aenter__(self) -> Self: + """Open the lock backend.""" + ... + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Close the lock backend.""" + ... + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock. + + Args: + name: The name of the lock. + token: The token to acquire the lock. + duration: The duration in seconds to hold the lock. + + Returns: + True if the lock is acquired, False if the lock is already acquired by another token. + + Raises: + Exception: Any exception can be raised if the lock cannot be acquired. + """ + ... + + async def release(self, *, name: str, token: str) -> bool: + """Release a lock. + + Args: + name: The name of the lock. + token: The token to release the lock. + + Returns: + True if the lock was released, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock cannot be released. + """ + ... + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired. + + Args: + name: The name of the lock. + + Returns: + True if the lock is acquired, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock status cannot be checked. + """ + ... + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned. + + Args: + name: The name of the lock. + token: The token to check. + + Returns: + True if the lock is owned by the token, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock status cannot be checked. + """ + ... + + +@runtime_checkable +class Synchronization(Protocol): + """Synchronization Primitive Protocol.""" + + async def __aenter__(self) -> Self: + """Enter the synchronization primitive.""" + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the synchronization primitive.""" + ... + + +Seconds = PositiveFloat diff --git a/.conflict-side-0/grelmicro/sync/errors.py b/.conflict-side-0/grelmicro/sync/errors.py new file mode 100644 index 0000000..6384e36 --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/errors.py @@ -0,0 +1,67 @@ +"""Grelmicro Synchronization Primitive Errors.""" + +from grelmicro.errors import SettingsValidationError + + +class SyncError(Exception): + """Synchronization Primitive Error. + + This the base class for all lock errors. + """ + + +class SyncBackendError(SyncError): + """Synchronization Backend Error.""" + + +class BackendNotLoadedError(SyncBackendError): + """Backend Not Loaded Error.""" + + def __init__(self, backend_name: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not load backend {backend_name}, try initializing one first" + ) + + +class LockAcquireError(SyncBackendError): + """Acquire Lock Error. + + This error is raised when an error on backend side occurs during lock acquisition. + """ + + def __init__(self, *, name: str, token: str) -> None: + """Initialize the error.""" + super().__init__(f"Failed to acquire lock: name={name}, token={token}") + + +class LockReleaseError(SyncBackendError): + """Lock Release Error. + + This error is raised when an error on backend side occurs during lock release. + """ + + def __init__( + self, *, name: str, token: str, reason: str | None = None + ) -> None: + """Initialize the error.""" + super().__init__( + f"Failed to release lock: name={name}, token={token}" + + (f", reason={reason}" if reason else ""), + ) + + +class LockNotOwnedError(LockReleaseError): + """Lock Not Owned Error during Release. + + This error is raised when an attempt is made to release a lock that is not owned, respectively + the token is different or the lock is already expired. + """ + + def __init__(self, *, name: str, token: str) -> None: + """Initialize the error.""" + super().__init__(name=name, token=token, reason="lock not owned") + + +class SyncSettingsValidationError(SyncError, SettingsValidationError): + """Synchronization Settings Validation Error.""" diff --git a/.conflict-side-0/grelmicro/sync/leaderelection.py b/.conflict-side-0/grelmicro/sync/leaderelection.py new file mode 100644 index 0000000..62ce539 --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/leaderelection.py @@ -0,0 +1,386 @@ +"""Leader Election.""" + +from logging import getLogger +from time import monotonic +from types import TracebackType +from typing import TYPE_CHECKING, Annotated, Self +from uuid import UUID, uuid1 + +from anyio import ( + TASK_STATUS_IGNORED, + CancelScope, + Condition, + fail_after, + get_cancelled_exc_class, + move_on_after, + sleep, +) +from anyio.abc import TaskStatus +from pydantic import BaseModel, model_validator +from typing_extensions import Doc + +from grelmicro.sync._backends import get_sync_backend +from grelmicro.sync.abc import Seconds, SyncBackend, Synchronization +from grelmicro.task.abc import Task + +if TYPE_CHECKING: + from contextlib import AsyncExitStack + + from anyio.abc import TaskGroup + +logger = getLogger("grelmicro.leader_election") + + +class LeaderElectionConfig(BaseModel): + """Leader Election Config. + + Leader election based on a leased reentrant distributed lock. + """ + + name: Annotated[ + str, + Doc( + """ + The leader election lock name. + """, + ), + ] + worker: Annotated[ + str | UUID, + Doc( + """ + The worker identity used as lock token. + """, + ), + ] + lease_duration: Annotated[ + Seconds, + Doc( + """ + The lease duration in seconds. + """, + ), + ] = 15 + renew_deadline: Annotated[ + Seconds, + Doc( + """ + The renew deadline in seconds. + """, + ), + ] = 10 + retry_interval: Annotated[ + Seconds, + Doc( + """ + The retry interval in seconds. + """, + ), + ] = 2 + backend_timeout: Annotated[ + Seconds, + Doc( + """ + The backend timeout in seconds. + """, + ), + ] = 5 + error_interval: Annotated[ + Seconds, + Doc( + """ + The error interval in seconds. + """, + ), + ] = 30 + + @model_validator(mode="after") + def _validate(self) -> Self: + if self.renew_deadline >= self.lease_duration: + msg = "Renew deadline must be shorter than lease duration" + raise ValueError(msg) + if self.retry_interval >= self.renew_deadline: + msg = "Retry interval must be shorter than renew deadline" + raise ValueError(msg) + if self.backend_timeout >= self.renew_deadline: + msg = "Backend timeout must be shorter than renew deadline" + raise ValueError(msg) + return self + + +class LeaderElection(Synchronization, Task): + """Leader Election. + + The leader election is a synchronization primitive with the worker as scope. + It runs as a task to acquire or renew the distributed lock. + """ + + def __init__( + self, + name: Annotated[ + str, + Doc( + """ + The name of the resource representing the leader election. + + It will be used as the lock name so make sure it is unique on the distributed lock + backend. + """, + ), + ], + *, + backend: Annotated[ + SyncBackend | None, + Doc( + """ + The distributed lock backend used to acquire and release the lock. + + By default, it will use the lock backend registry to get the default lock backend. + """, + ), + ] = None, + worker: Annotated[ + str | UUID | None, + Doc( + """ + The worker identity. + + By default, use a UUIDv1 will be generated. + """, + ), + ] = None, + lease_duration: Annotated[ + Seconds, + Doc( + """ + The duration in seconds after the lock will be released if not renewed. + + If the worker becomes unavailable, the lock can only be acquired by an other worker + after it' has expired. + """, + ), + ] = 15, + renew_deadline: Annotated[ + Seconds, + Doc( + """ + The duration in seconds that the leader worker will try to acquire the lock before + giving up. + + Must be shorter than the lease duration. In case of multiple failures, the leader + worker will loose the lead to prevent split-brain scenarios and ensure that only one + worker is the leader at any time. + """, + ), + ] = 10, + retry_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between attempts to acquire or renew the lock. + + Must be shorter than the renew deadline. A shorter schedule enables faster leader + elections but may increase load on the distributed lock backend, while a longer + schedule reduces load but can delay new leader elections. + """, + ), + ] = 2, + backend_timeout: Annotated[ + Seconds, + Doc( + """ + The duration in seconds for waiting on backend for acquiring and releasing the lock. + + This value determines how long the system will wait before giving up the current + operation. + """, + ), + ] = 5, + error_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between logging error messages. + + If shorter than the retry interval, it will log every error. It is used to prevent + flooding the logs when the lock backend is unavailable. + """, + ), + ] = 30, + ) -> None: + """Initialize the leader election.""" + self.config = LeaderElectionConfig( + name=name, + worker=worker or uuid1(), + lease_duration=lease_duration, + renew_deadline=renew_deadline, + retry_interval=retry_interval, + backend_timeout=backend_timeout, + error_interval=error_interval, + ) + self.backend = backend or get_sync_backend() + + self._service_running = False + self._state_change_condition: Condition = Condition() + self._is_leader: bool = False + self._state_updated_at: float = monotonic() + self._error_logged_at: float | None = None + self._task_group: TaskGroup | None = None + self._exit_stack: AsyncExitStack | None = None + + async def __aenter__(self) -> Self: + """Wait for the leader with the context manager.""" + await self.wait_for_leader() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the context manager.""" + + @property + def name(self) -> str: + """Return the task name.""" + return self.config.name + + def is_running(self) -> bool: + """Check if the leader election task is running.""" + return self._service_running + + def is_leader(self) -> bool: + """Check if the current worker is the leader. + + To avoid a split-brain scenario, the leader considers itself as no longer leader if the + renew deadline is reached. + + Returns: + True if the current worker is the leader, False otherwise. + + """ + if not self._is_leader: + return False + return not self._is_renew_deadline_reached() + + async def wait_for_leader(self) -> None: + """Wait until the current worker is the leader.""" + while not self.is_leader(): + async with self._state_change_condition: + await self._state_change_condition.wait() + + async def wait_lose_leader(self) -> None: + """Wait until the current worker is no longer the leader.""" + while self.is_leader(): + with move_on_after(self._seconds_before_expiration_deadline()): + async with self._state_change_condition: + await self._state_change_condition.wait() + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run polling loop service to acquire or renew the distributed lock.""" + task_status.started() + if self._service_running: + logger.warning("Leader Election already running: %s", self.name) + return + self._service_running = True + logger.info("Leader Election started: %s", self.name) + try: + while True: + await self._try_acquire_or_renew() + await sleep(self.config.retry_interval) + except get_cancelled_exc_class(): + logger.info("Leader Election stopped: %s", self.name) + raise + except BaseException: + logger.exception("Leader Election crashed: %s", self.name) + raise + finally: + self._service_running = False + with CancelScope(shield=True): + await self._release() + + async def _update_state( + self, *, is_leader: bool, raison_if_no_more_leader: str + ) -> None: + """Update the state of the leader election.""" + self._state_updated_at = monotonic() + if is_leader is self._is_leader: + return # No change + + self._is_leader = is_leader + + if is_leader: + logger.info("Leader Election acquired leadership: %s", self.name) + else: + logger.warning( + "Leader Election lost leadership: %s (%s)", + self.name, + raison_if_no_more_leader, + ) + + async with self._state_change_condition: + self._state_change_condition.notify_all() + + async def _try_acquire_or_renew(self) -> None: + """Try to acquire leadership.""" + try: + with fail_after(self.config.backend_timeout): + is_leader = await self.backend.acquire( + name=self.name, + token=str(self.config.worker), + duration=self.config.lease_duration, + ) + except Exception: + if self._check_error_interval(): + logger.exception( + "Leader Election failed to acquire lock: %s", self.name + ) + if self._is_renew_deadline_reached(): + await self._update_state( + is_leader=False, + raison_if_no_more_leader="renew deadline reached", + ) + else: + await self._update_state( + is_leader=is_leader, + raison_if_no_more_leader="lock not acquired", + ) + + def _seconds_before_expiration_deadline(self) -> float: + return max( + self._state_updated_at + self.config.lease_duration - monotonic(), 0 + ) + + def _check_error_interval(self) -> bool: + """Check if the cooldown interval allows to log the error.""" + is_logging_allowed = ( + not self._error_logged_at + or (monotonic() - self._error_logged_at) + > self.config.error_interval + ) + self._error_logged_at = monotonic() + return is_logging_allowed + + def _is_renew_deadline_reached(self) -> bool: + return ( + monotonic() - self._state_updated_at + ) >= self.config.renew_deadline + + async def _release(self) -> None: + try: + with fail_after(self.config.backend_timeout): + if not ( + await self.backend.release( + name=self.config.name, token=str(self.config.worker) + ) + ): + logger.info( + "Leader Election lock already released: %s", self.name + ) + except Exception: + logger.exception( + "Leader Election failed to release lock: %s", self.name + ) diff --git a/.conflict-side-0/grelmicro/sync/lock.py b/.conflict-side-0/grelmicro/sync/lock.py new file mode 100644 index 0000000..c87d08f --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/lock.py @@ -0,0 +1,324 @@ +"""Grelmicro Lock.""" + +from time import sleep as thread_sleep +from types import TracebackType +from typing import Annotated, Self +from uuid import UUID, uuid1 + +from anyio import WouldBlock, from_thread, sleep +from typing_extensions import Doc + +from grelmicro.sync._backends import get_sync_backend +from grelmicro.sync._base import BaseLock, BaseLockConfig +from grelmicro.sync._utils import generate_task_token, generate_thread_token +from grelmicro.sync.abc import Seconds, SyncBackend +from grelmicro.sync.errors import ( + LockAcquireError, + LockNotOwnedError, + LockReleaseError, + SyncBackendError, +) + + +class LockConfig(BaseLockConfig, frozen=True, extra="forbid"): + """Lock Config.""" + + lease_duration: Annotated[ + Seconds, + Doc( + """ + The lease duration in seconds for the lock. + """, + ), + ] + retry_interval: Annotated[ + Seconds, + Doc( + """ + The interval in seconds between attempts to acquire the lock. + """, + ), + ] + + +class Lock(BaseLock): + """Lock. + + This lock is a distributed lock that is used to acquire a resource across multiple workers. The + lock is acquired asynchronously and can be extended multiple times manually. The lock is + automatically released after a duration if not extended. + """ + + def __init__( + self, + name: Annotated[ + str, + Doc( + """ + The name of the resource to lock. + + It will be used as the lock name so make sure it is unique on the lock backend. + """, + ), + ], + *, + backend: Annotated[ + SyncBackend | None, + Doc(""" + The distributed lock backend used to acquire and release the lock. + + By default, it will use the lock backend registry to get the default lock backend. + """), + ] = None, + worker: Annotated[ + str | UUID | None, + Doc( + """ + The worker identity. + + By default, use a UUIDv1 will be generated. + """, + ), + ] = None, + lease_duration: Annotated[ + Seconds, + Doc( + """ + The duration in seconds for the lock to be held by default. + """, + ), + ] = 60, + retry_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between attempts to acquire the lock. + + Should be greater or equal than 0.1 to prevent flooding the lock backend. + """, + ), + ] = 0.1, + ) -> None: + """Initialize the lock.""" + self._config: LockConfig = LockConfig( + name=name, + worker=worker or uuid1(), + lease_duration=lease_duration, + retry_interval=retry_interval, + ) + self.backend = backend or get_sync_backend() + self._from_thread: ThreadLockAdapter | None = None + + async def __aenter__(self) -> Self: + """Acquire the lock with the async context manager.""" + await self.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock with the async context manager. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + await self.release() + + @property + def config(self) -> LockConfig: + """Return the lock config.""" + return self._config + + @property + def from_thread(self) -> "ThreadLockAdapter": + """Return the lock adapter for worker thread.""" + if self._from_thread is None: + self._from_thread = ThreadLockAdapter(lock=self) + return self._from_thread + + async def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + while not await self.do_acquire(token=token): # noqa: ASYNC110 // Polling is intentional + await sleep(self._config.retry_interval) + + async def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + WouldBlock: If the lock cannot be acquired without blocking. + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + if not await self.do_acquire(token=token): + msg = f"Lock not acquired: name={self._config.name}, token={token}" + raise WouldBlock(msg) + + async def release(self) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + if not await self.do_release(token): + raise LockNotOwnedError(name=self._config.name, token=token) + + async def locked(self) -> bool: + """Check if the lock is acquired. + + Raise: + SyncBackendError: If the lock cannot be checked due to an error on the backend. + """ + try: + return await self.backend.locked(name=self._config.name) + except Exception as exc: + msg = "Failed to check if the lock is acquired" + raise SyncBackendError(msg) from exc + + async def owned(self) -> bool: + """Check if the lock is owned by the current token. + + Raise: + SyncBackendError: If the lock cannot be checked due to an error on the backend. + """ + return await self.do_owned(generate_task_token(self._config.worker)) + + async def do_acquire(self, token: str) -> bool: + """Acquire the lock. + + This method should not be called directly. Use `acquire` instead. + + Returns: + bool: True if the lock was acquired, False if the lock was not acquired. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + """ + try: + return await self.backend.acquire( + name=self._config.name, + token=token, + duration=self._config.lease_duration, + ) + except Exception as exc: + raise LockAcquireError(name=self._config.name, token=token) from exc + + async def do_release(self, token: str) -> bool: + """Release the lock. + + This method should not be called directly. Use `release` instead. + + Returns: + bool: True if the lock was released, False otherwise. + + Raises: + LockReleaseError: Cannot release the lock due to backend error. + """ + try: + return await self.backend.release( + name=self._config.name, token=token + ) + except Exception as exc: + raise LockReleaseError(name=self._config.name, token=token) from exc + + async def do_owned(self, token: str) -> bool: + """Check if the lock is owned by the current token. + + This method should not be called directly. Use `owned` instead. + + Returns: + bool: True if the lock is owned by the current token, False otherwise. + + Raises: + SyncBackendError: Cannot check if the lock is owned due to backend error. + """ + try: + return await self.backend.owned(name=self._config.name, token=token) + except Exception as exc: + msg = "Failed to check if the lock is owned" + raise SyncBackendError(msg) from exc + + +class ThreadLockAdapter: + """Lock Adapter for Worker Thread.""" + + def __init__(self, lock: Lock) -> None: + """Initialize the lock adapter.""" + self._lock = lock + + def __enter__(self) -> Self: + """Acquire the lock with the context manager.""" + self.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock with the context manager.""" + self.release() + + def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: Cannot acquire the lock due to backend error. + + """ + token = generate_thread_token(self._lock.config.worker) + retry_interval = self._lock.config.retry_interval + while not from_thread.run(self._lock.do_acquire, token): + thread_sleep(retry_interval) + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + LockAcquireError: Cannot acquire the lock due to backend error. + WouldBlock: If the lock cannot be acquired without blocking. + + """ + token = generate_thread_token(self._lock.config.worker) + if not from_thread.run(self._lock.do_acquire, token): + msg = f"Lock not acquired: name={self._lock.config.name}, token={token}" + raise WouldBlock(msg) + + def release(self) -> None: + """Release the lock. + + Raises: + ReleaseSyncBackendError: Cannot release the lock due to backend error. + LockNotOwnedError: If the lock is not currently held. + + """ + token = generate_thread_token(self._lock.config.worker) + if not from_thread.run(self._lock.do_release, token): + raise LockNotOwnedError(name=self._lock.config.name, token=token) + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + return from_thread.run(self._lock.locked) + + def owned(self) -> bool: + """Return True if the lock is currently held by the current worker thread.""" + return from_thread.run( + self._lock.do_owned, generate_thread_token(self._lock.config.worker) + ) diff --git a/.conflict-side-0/grelmicro/sync/memory.py b/.conflict-side-0/grelmicro/sync/memory.py new file mode 100644 index 0000000..9746c59 --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/memory.py @@ -0,0 +1,78 @@ +"""Memory Synchronization Backend.""" + +from time import monotonic +from types import TracebackType +from typing import Annotated, Self + +from typing_extensions import Doc + +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend + + +class MemorySyncBackend(SyncBackend): + """Memory Synchronization Backend. + + This is not a backend with a real distributed lock. It is a local lock that can be used for + testing purposes or for locking operations that are executed in the same AnyIO event loop. + """ + + def __init__( + self, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + ) -> None: + """Initialize the lock backend.""" + self._locks: dict[str, tuple[str | None, float]] = {} + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Enter the lock backend.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Exit the lock backend.""" + self._locks.clear() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + if ( + current_token is None + or current_token == token + or expire_at < monotonic() + ): + self._locks[name] = (token, monotonic() + duration) + return True + return False + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + if current_token == token and expire_at >= monotonic(): + del self._locks[name] + return True + if current_token and expire_at < monotonic(): + del self._locks[name] + return False + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + return current_token is not None and expire_at >= monotonic() + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + return current_token == token and expire_at >= monotonic() diff --git a/.conflict-side-0/grelmicro/sync/postgres.py b/.conflict-side-0/grelmicro/sync/postgres.py new file mode 100644 index 0000000..35b32a1 --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/postgres.py @@ -0,0 +1,198 @@ +"""PostgreSQL Synchronization Backend.""" + +from types import TracebackType +from typing import Annotated, Self + +from asyncpg import Pool, create_pool +from pydantic import PostgresDsn +from pydantic_core import MultiHostUrl, ValidationError +from pydantic_settings import BaseSettings +from typing_extensions import Doc + +from grelmicro.errors import OutOfContextError +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import SyncSettingsValidationError + + +class _PostgresSettings(BaseSettings): + POSTGRES_HOST: str | None = None + POSTGRES_PORT: int = 5432 + POSTGRES_DB: str | None = None + POSTGRES_USER: str | None = None + POSTGRES_PASSWORD: str | None = None + POSTGRES_URL: PostgresDsn | None = None + + def url(self) -> str: + """Generate the Postgres URL from the parts.""" + if self.POSTGRES_URL: + return self.POSTGRES_URL.unicode_string() + + if all( + ( + self.POSTGRES_HOST, + self.POSTGRES_DB, + self.POSTGRES_USER, + self.POSTGRES_PASSWORD, + ) + ): + return MultiHostUrl.build( + scheme="postgresql", + username=self.POSTGRES_USER, + password=self.POSTGRES_PASSWORD, + host=self.POSTGRES_HOST, + port=self.POSTGRES_PORT, + path=self.POSTGRES_DB, + ).unicode_string() + + msg = ( + "Either POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and " + "POSTGRES_PASSWORD must be set" + ) + raise SyncSettingsValidationError(msg) + + +class PostgresSyncBackend(SyncBackend): + """PostgreSQL Synchronization Backend.""" + + _SQL_CREATE_TABLE_IF_NOT_EXISTS = """ + CREATE TABLE IF NOT EXISTS {table_name} ( + name TEXT PRIMARY KEY, + token TEXT NOT NULL, + expire_at TIMESTAMP NOT NULL + ); + """ + + _SQL_ACQUIRE_OR_EXTEND = """ + INSERT INTO {table_name} (name, token, expire_at) + VALUES ($1, $2, NOW() + make_interval(secs => $3)) + ON CONFLICT (name) DO UPDATE + SET token = EXCLUDED.token, expire_at = EXCLUDED.expire_at + WHERE {table_name}.token = EXCLUDED.token OR {table_name}.expire_at < NOW() + RETURNING 1; + """ + + _SQL_RELEASE = """ + DELETE FROM {table_name} + WHERE name = $1 AND token = $2 AND expire_at >= NOW() + RETURNING 1; + """ + + _SQL_RELEASE_ALL_EXPIRED = """ + DELETE FROM {table_name} + WHERE expire_at < NOW(); + """ + + _SQL_LOCKED = """ + SELECT 1 FROM {table_name} + WHERE name = $1 AND expire_at >= NOW(); + """ + + _SQL_OWNED = """ + SELECT 1 FROM {table_name} + WHERE name = $1 AND token = $2 AND expire_at >= NOW(); + """ + + def __init__( + self, + url: Annotated[ + PostgresDsn | str | None, + Doc(""" + The Postgres database URL. + + If not provided, the URL will be taken from the environment variables POSTGRES_URL + or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD. + """), + ] = None, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + table_name: Annotated[ + str, Doc("The table name to store the locks.") + ] = "locks", + ) -> None: + """Initialize the lock backend.""" + if not table_name.isidentifier(): + msg = f"Table name '{table_name}' is not a valid identifier" + raise ValueError(msg) + + try: + self._url = url or _PostgresSettings().url() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + self._table_name = table_name + self._acquire_sql = self._SQL_ACQUIRE_OR_EXTEND.format( + table_name=table_name + ) + self._release_sql = self._SQL_RELEASE.format(table_name=table_name) + self._pool: Pool | None = None + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Enter the lock backend.""" + self._pool = await create_pool(str(self._url)) + await self._pool.execute( + self._SQL_CREATE_TABLE_IF_NOT_EXISTS.format( + table_name=self._table_name + ), + ) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Exit the lock backend.""" + if self._pool: + await self._pool.execute( + self._SQL_RELEASE_ALL_EXPIRED.format( + table_name=self._table_name + ), + ) + await self._pool.close() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire a lock.""" + if not self._pool: + raise OutOfContextError(self, "acquire") + + return bool( + await self._pool.fetchval(self._acquire_sql, name, token, duration) + ) + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + if not self._pool: + raise OutOfContextError(self, "release") + return bool(await self._pool.fetchval(self._release_sql, name, token)) + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + if not self._pool: + raise OutOfContextError(self, "locked") + return bool( + await self._pool.fetchval( + self._SQL_LOCKED.format(table_name=self._table_name), + name, + ), + ) + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + if not self._pool: + raise OutOfContextError(self, "owned") + return bool( + await self._pool.fetchval( + self._SQL_OWNED.format(table_name=self._table_name), + name, + token, + ), + ) diff --git a/.conflict-side-0/grelmicro/sync/redis.py b/.conflict-side-0/grelmicro/sync/redis.py new file mode 100644 index 0000000..73090c8 --- /dev/null +++ b/.conflict-side-0/grelmicro/sync/redis.py @@ -0,0 +1,146 @@ +"""Redis Synchronization Backend.""" + +from types import TracebackType +from typing import Annotated, Self + +from pydantic import RedisDsn, ValidationError +from pydantic_core import Url +from pydantic_settings import BaseSettings +from redis.asyncio.client import Redis +from typing_extensions import Doc + +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import SyncSettingsValidationError + + +class _RedisSettings(BaseSettings): + """Redis settings from the environment variables.""" + + REDIS_HOST: str | None = None + REDIS_PORT: int = 6379 + REDIS_DB: int = 0 + REDIS_PASSWORD: str | None = None + REDIS_URL: RedisDsn | None = None + + +def _get_redis_url() -> str: + """Get the Redis URL from the environment variables. + + Raises: + SyncSettingsValidationError: If the URL or host is not set. + """ + try: + settings = _RedisSettings() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + if settings.REDIS_URL and not settings.REDIS_HOST: + return settings.REDIS_URL.unicode_string() + + if settings.REDIS_HOST and not settings.REDIS_URL: + return Url.build( + scheme="redis", + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + path=str(settings.REDIS_DB), + password=settings.REDIS_PASSWORD, + ).unicode_string() + + msg = "Either REDIS_URL or REDIS_HOST must be set" + raise SyncSettingsValidationError(msg) + + +class RedisSyncBackend(SyncBackend): + """Redis Synchronization Backend.""" + + _LUA_ACQUIRE_OR_EXTEND = """ + local token = redis.call('get', KEYS[1]) + if not token then + redis.call('set', KEYS[1], ARGV[1], 'px', ARGV[2]) + return 1 + end + if token == ARGV[1] then + redis.call('pexpire', KEYS[1], ARGV[2]) + return 1 + end + return 0 + """ + _LUA_RELEASE = """ + local token = redis.call('get', KEYS[1]) + if not token or token ~= ARGV[1] then + return 0 + end + redis.call('del', KEYS[1]) + return 1 + """ + + def __init__( + self, + url: Annotated[ + RedisDsn | str | None, + Doc(""" + The Redis URL. + + If not provided, the URL will be taken from the environment variables REDIS_URL + or REDIS_HOST, REDIS_PORT, REDIS_DB, and REDIS_PASSWORD. + """), + ] = None, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + ) -> None: + """Initialize the lock backend.""" + self._url = url or _get_redis_url() + self._redis: Redis = Redis.from_url(str(self._url)) + self._lua_release = self._redis.register_script(self._LUA_RELEASE) + self._lua_acquire = self._redis.register_script( + self._LUA_ACQUIRE_OR_EXTEND + ) + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Open the lock backend.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Close the lock backend.""" + await self._redis.aclose() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock.""" + return bool( + await self._lua_acquire( + keys=[name], + args=[token, int(duration * 1000)], + client=self._redis, + ) + ) + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + return bool( + await self._lua_release( + keys=[name], args=[token], client=self._redis + ) + ) + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + return bool(await self._redis.get(name)) + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + return bool( + (await self._redis.get(name)) == token.encode() + ) # redis returns bytes diff --git a/.conflict-side-0/grelmicro/task/__init__.py b/.conflict-side-0/grelmicro/task/__init__.py new file mode 100644 index 0000000..374bf08 --- /dev/null +++ b/.conflict-side-0/grelmicro/task/__init__.py @@ -0,0 +1,6 @@ +"""Grelmicro Task Scheduler.""" + +from grelmicro.task.manager import TaskManager +from grelmicro.task.router import TaskRouter + +__all__ = ["TaskManager", "TaskRouter"] diff --git a/.conflict-side-0/grelmicro/task/_interval.py b/.conflict-side-0/grelmicro/task/_interval.py new file mode 100644 index 0000000..f66c2f2 --- /dev/null +++ b/.conflict-side-0/grelmicro/task/_interval.py @@ -0,0 +1,92 @@ +"""Interval Task.""" + +from collections.abc import Awaitable, Callable +from contextlib import nullcontext +from functools import partial +from inspect import iscoroutinefunction +from logging import getLogger +from typing import Any + +from anyio import TASK_STATUS_IGNORED, sleep, to_thread +from anyio.abc import TaskStatus +from fast_depends import inject + +from grelmicro.sync.abc import Synchronization +from grelmicro.task._utils import validate_and_generate_reference +from grelmicro.task.abc import Task + +logger = getLogger("grelmicro.task") + + +class IntervalTask(Task): + """Interval Task. + + Use the `TaskManager.interval()` or `SchedulerRouter.interval()` decorator instead + of creating IntervalTask objects directly. + """ + + def __init__( + self, + *, + function: Callable[..., Any], + name: str | None = None, + interval: float, + sync: Synchronization | None = None, + ) -> None: + """Initialize the IntervalTask. + + Raises: + FunctionNotSupportedError: If the function is not supported. + ValueError: If internal is less than or equal to 0. + """ + if interval <= 0: + msg = "Interval must be greater than 0" + raise ValueError(msg) + + alt_name = validate_and_generate_reference(function) + self._name = name or alt_name + self._interval = interval + self._async_function = self._prepare_async_function(function) + self._sync = sync if sync else nullcontext() + + @property + def name(self) -> str: + """Return the lock name.""" + return self._name + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run the repeated task loop.""" + logger.info( + "Task started (interval: %ss): %s", self._interval, self.name + ) + task_status.started() + try: + while True: + try: + async with self._sync: + try: + await self._async_function() + except Exception: + logger.exception( + "Task execution error: %s", self.name + ) + except Exception: + logger.exception( + "Task synchronization error: %s", self.name + ) + await sleep(self._interval) + finally: + logger.info("Task stopped: %s", self.name) + + def _prepare_async_function( + self, function: Callable[..., Any] + ) -> Callable[..., Awaitable[Any]]: + """Prepare the function with lock and ensure async function.""" + function = inject(function) + return ( + function + if iscoroutinefunction(function) + else partial(to_thread.run_sync, function) + ) diff --git a/.conflict-side-0/grelmicro/task/_utils.py b/.conflict-side-0/grelmicro/task/_utils.py new file mode 100644 index 0000000..7cfec3f --- /dev/null +++ b/.conflict-side-0/grelmicro/task/_utils.py @@ -0,0 +1,43 @@ +"""Task Utilities.""" + +from collections.abc import Callable +from functools import partial +from inspect import ismethod +from typing import Any + +from grelmicro.task.errors import FunctionTypeError + + +def validate_and_generate_reference(function: Callable[..., Any]) -> str: + """Generate a task name from the given function. + + This implementation is inspirated by the APScheduler project under MIT License. + Original source: https://github.com/agronholm/apscheduler/blob/master/src/apscheduler/_marshalling.py + + Raises: + FunctionNotSupportedError: If function is not supported. + + """ + if isinstance(function, partial): + ref = "partial()" + raise FunctionTypeError(ref) + + if ismethod(function): + ref = "method" + raise FunctionTypeError(ref) + + if not hasattr(function, "__module__") or not hasattr( + function, "__qualname__" + ): + ref = "callable without __module__ or __qualname__ attribute" + raise FunctionTypeError(ref) + + if "" in function.__qualname__: + ref = "lambda" + raise FunctionTypeError(ref) + + if "" in function.__qualname__: + ref = "nested function" + raise FunctionTypeError(ref) + + return f"{function.__module__}:{function.__qualname__}" diff --git a/.conflict-side-0/grelmicro/task/abc.py b/.conflict-side-0/grelmicro/task/abc.py new file mode 100644 index 0000000..d4e7cf3 --- /dev/null +++ b/.conflict-side-0/grelmicro/task/abc.py @@ -0,0 +1,31 @@ +"""Grelmicro Task Synchronization Abstract Base Classes and Protocols.""" + +from typing import Protocol + +from anyio import TASK_STATUS_IGNORED +from anyio.abc import TaskStatus +from typing_extensions import runtime_checkable + + +@runtime_checkable +class Task(Protocol): + """Task Protocol. + + A task that runs in background in the async event loop. + """ + + @property + def name(self) -> str: + """Name to uniquely identify the task.""" + ... + + async def __call__( + self, + *, + task_status: TaskStatus[None] = TASK_STATUS_IGNORED, + ) -> None: + """Run the task. + + This is the entry point of the task to be run in the async event loop. + """ + ... diff --git a/.conflict-side-0/grelmicro/task/errors.py b/.conflict-side-0/grelmicro/task/errors.py new file mode 100644 index 0000000..a788f61 --- /dev/null +++ b/.conflict-side-0/grelmicro/task/errors.py @@ -0,0 +1,28 @@ +"""Grelmicro Task Scheduler Errors.""" + +from grelmicro.errors import GrelmicroError + + +class TaskError(GrelmicroError): + """Base Grelmicro Task error.""" + + +class FunctionTypeError(TaskError, TypeError): + """Function Type Error.""" + + def __init__(self, reference: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not use function {reference}, " + "try declaring 'def' or 'async def' directly in the module" + ) + + +class TaskAddOperationError(TaskError, RuntimeError): + """Task Add Operation Error.""" + + def __init__(self) -> None: + """Initialize the error.""" + super().__init__( + "Could not add the task, try calling 'add_task' and 'include_router' before starting" + ) diff --git a/.conflict-side-0/grelmicro/task/manager.py b/.conflict-side-0/grelmicro/task/manager.py new file mode 100644 index 0000000..5432145 --- /dev/null +++ b/.conflict-side-0/grelmicro/task/manager.py @@ -0,0 +1,89 @@ +"""Grelmicro Task Manager.""" + +from contextlib import AsyncExitStack +from logging import getLogger +from types import TracebackType +from typing import TYPE_CHECKING, Annotated, Self + +from anyio import create_task_group +from typing_extensions import Doc + +from grelmicro.errors import OutOfContextError +from grelmicro.task.abc import Task +from grelmicro.task.errors import TaskAddOperationError +from grelmicro.task.router import TaskRouter + +if TYPE_CHECKING: + from anyio.abc import TaskGroup + +logger = getLogger("grelmicro.task") + + +class TaskManager(TaskRouter): + """Task Manager. + + `TaskManager` class, the main entrypoint to manage scheduled tasks. + """ + + def __init__( + self, + *, + auto_start: Annotated[ + bool, + Doc( + """ + Automatically start all tasks. + """, + ), + ] = True, + tasks: Annotated[ + list[Task] | None, + Doc( + """ + A list of tasks to be started. + """, + ), + ] = None, + ) -> None: + """Initialize the task manager.""" + TaskRouter.__init__(self, tasks=tasks) + + self._auto_start = auto_start + self._task_group: TaskGroup | None = None + + async def __aenter__(self) -> Self: + """Enter the context manager.""" + self._exit_stack = AsyncExitStack() + await self._exit_stack.__aenter__() + self._task_group = await self._exit_stack.enter_async_context( + create_task_group(), + ) + if self._auto_start: + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the context manager.""" + if not self._task_group or not self._exit_stack: + raise OutOfContextError(self, "__aexit__") + self._task_group.cancel_scope.cancel() + return await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb) + + async def start(self) -> None: + """Start all tasks manually.""" + if not self._task_group: + raise OutOfContextError(self, "start") + + if self._started: + raise TaskAddOperationError + + self.do_mark_as_started() + + for task in self.tasks: + await self._task_group.start(task.__call__) + logger.debug("%s scheduled tasks started", len(self._tasks)) diff --git a/.conflict-side-0/grelmicro/task/router.py b/.conflict-side-0/grelmicro/task/router.py new file mode 100644 index 0000000..16b240d --- /dev/null +++ b/.conflict-side-0/grelmicro/task/router.py @@ -0,0 +1,132 @@ +"""Grelmicro Task Router.""" + +from collections.abc import Awaitable, Callable +from typing import Annotated, Any + +from typing_extensions import Doc + +from grelmicro.sync.abc import Synchronization +from grelmicro.task.abc import Task +from grelmicro.task.errors import TaskAddOperationError + + +class TaskRouter: + """Task Router. + + `TaskRouter` class, used to group task schedules, for example to structure an app in + multiple files. It would then included in the `TaskManager`, or in another + `TaskRouter`. + """ + + def __init__( + self, + *, + tasks: Annotated[ + list[Task] | None, + Doc( + """ + A list of schedules or scheduled tasks to be scheduled. + """, + ), + ] = None, + ) -> None: + """Initialize the task router.""" + self._started = False + self._tasks: list[Task] = tasks or [] + self._routers: list[TaskRouter] = [] + + @property + def tasks(self) -> list[Task]: + """List of scheduled tasks.""" + return self._tasks + [ + task for router in self._routers for task in router.tasks + ] + + def add_task(self, task: Task) -> None: + """Add a task to the scheduler.""" + if self._started: + raise TaskAddOperationError + + self._tasks.append(task) + + def interval( + self, + *, + seconds: Annotated[ + float, + Doc( + """ + The duration in seconds between each task run. + + Accuracy is not guaranteed and may vary with system load. Consider the + execution time of the task when setting the interval. + """, + ), + ], + name: Annotated[ + str | None, + Doc( + """ + The name of the task. + + If None, a name will be generated automatically from the function. + """, + ), + ] = None, + sync: Annotated[ + Synchronization | None, + Doc( + """ + The synchronization primitive to use for the task. + + You can use a `LeasedLock` or a `LeaderElection`, for example. If None, + no synchronization is used and the task will run on all workers. + """, + ), + ] = None, + ) -> Callable[ + [Callable[..., Any | Awaitable[Any]]], + Callable[..., Any | Awaitable[Any]], + ]: + """Decorate function to add it to the task scheduler. + + Raises: + TaskNameGenerationError: If the task name generation fails. + """ + from grelmicro.task._interval import IntervalTask + + def decorator( + function: Callable[[], None | Awaitable[None]], + ) -> Callable[[], None | Awaitable[None]]: + self.add_task( + IntervalTask( + name=name, + function=function, + interval=seconds, + sync=sync, + ), + ) + return function + + return decorator + + def include_router(self, router: "TaskRouter") -> None: + """Include another router in this router.""" + if self._started: + raise TaskAddOperationError + + self._routers.append(router) + + def started(self) -> bool: + """Check if the task manager has started.""" + return self._started + + def do_mark_as_started(self) -> None: + """Mark the task manager as started. + + Do not call this method directly. It is called by the task manager when the task + manager is started. + """ + self._started = True + for router in self._routers: + router.do_mark_as_started() diff --git a/.conflict-side-0/mkdocs.yml b/.conflict-side-0/mkdocs.yml new file mode 100644 index 0000000..0b08e9f --- /dev/null +++ b/.conflict-side-0/mkdocs.yml @@ -0,0 +1,47 @@ +site_name: Grelmicro +site_description: Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. +site_url: https://grelmicro.grel.info +theme: + name: material + palette: + primary: green + accent: light green + font: + text: 'Roboto' + code: 'Roboto Mono' + features: + - content.tabs.link + - content.code.copy + - content.code.select + - content.tooltips + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.progress + - navigation.top + - navigation.tracking + +repo_name: grelinfo/grelmicro +repo_url: https://github.com/grelinfo/grelmicro + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + +nav: +- Grelmicro: index.md +- User Guide: + - logging.md + - sync.md + - task.md + +markdown_extensions: + - admonition + - mdx_include: + base_path: docs + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.inlinehilite + - pymdownx.tabbed: + alternate_style: true diff --git a/.conflict-side-0/pyproject.toml b/.conflict-side-0/pyproject.toml new file mode 100644 index 0000000..9bcca87 --- /dev/null +++ b/.conflict-side-0/pyproject.toml @@ -0,0 +1,174 @@ +[project] +name = "grelmicro" +description = "Grelmicro is a lightweight framework/toolkit for building async microservices in Python" +license = "MIT" +authors = [{ name = "Loïc Gremaud", email = "grelinfo@gmail.com"}] +readme = "README.md" + +classifiers = [ + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", + "Typing :: Typed", + "Development Status :: 1 - Planning", + "Environment :: Web Environment", + "Framework :: AsyncIO", + "Framework :: FastAPI", + "Framework :: Pydantic", + "Framework :: Pydantic :: 2", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dynamic = ["version"] + +requires-python = ">=3.11" + +dependencies = [ + "anyio>=4.0.0", + "pydantic>=2.5.0", + "fast-depends>=2.0.0", + "pydantic-settings>=2.5.0", +] + +[project.urls] + +Repository = "https://github.com/grelinfo/grelmicro.git" +Issues = "https://github.com/grelinfo/grelmicro/issues" + +[project.optional-dependencies] +standard = [ + "loguru>=0.7.2", + "orjson>=3.10.11", +] +postgres = [ + "asyncpg>=0.30.0", +] +redis = [ + "redis>=5.0.0", +] + +[dependency-groups] +dev = [ + "pytest-cov>=6.0.0", + "pytest>=8.0.0", + "mypy>=1.12.0", + "ruff>=0.7.4", + "testcontainers[postgres,redis]>=4.8.2", + "pytest-timeout>=2.3.1", + "pytest-mock>=3.14.0", + "pytest-randomly>=3.16.0", + "pre-commit>=4.0.1", + "fastapi>=0.115.5", + "fastapi-cli>=0.0.5", + "mdx-include>=1.4.2", + "faststream>=0.5.30", + "hatch>=1.13.0", +] +docs = [ + "mkdocs-material>=9.5.44", + "pygments>=2.18.0", + "pymdown-extensions>=10.12", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build] +skip-excluded-dirs = true +exclude = ["/tests", "/docs", "/examples"] + +[tool.hatch.version] +path = "grelmicro/__init__.py" + +[tool.ruff] +target-version = "py311" +line-length = 80 + +[tool.ruff.lint] +select = ["ALL"] +ignore = ["COM812", "ISC001"] # Ignore rules conflicting with the formatter. + +[tool.ruff.lint.extend-per-file-ignores] +"examples/*" = [ + "ARG001", + "ANN001", + "ANN201", + "D103", + "D100", + "INP001", + "T201", +] +"examples/logging/basic.py" = ["EM101", "TRY"] +"examples/task/router.py" = ["I001", "E402"] +"tests/*" = [ + "S101", + "SLF001" +] + +[tool.ruff.lint.pycodestyle] +max-line-length = 100 # reports only line that exceed 100 characters. + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.pylint] +max-args = 10 + +[tool.mypy] +scripts_are_modules = true +plugins = [ + "pydantic.mypy" +] +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = true +check_untyped_defs = true +no_implicit_reexport = true +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = ["asyncpg", "testcontainers.*"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = [ + "examples.*" +] +disallow_untyped_defs = false + + +[tool.pytest.ini_options] +addopts = """ + --cov=grelmicro + --cov-report term:skip-covered + --cov-report xml:cov.xml + --strict-config + --strict-markers + -m "not integration" +""" +markers = """ + integration: mark a test as an integration test (disabled by default). +""" + +testpaths = "tests" + +[tool.coverage.report] +sort = "-Cover" +exclude_also = [ + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "assert_never\\(.*\\)", +] diff --git a/.conflict-side-0/tests/__init__.py b/.conflict-side-0/tests/__init__.py new file mode 100644 index 0000000..adc28b2 --- /dev/null +++ b/.conflict-side-0/tests/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Tests.""" diff --git a/.conflict-side-0/tests/conftest.py b/.conflict-side-0/tests/conftest.py new file mode 100644 index 0000000..916c148 --- /dev/null +++ b/.conflict-side-0/tests/conftest.py @@ -0,0 +1,9 @@ +"""Grelmicro Test Config.""" + +import pytest + + +@pytest.fixture +def anyio_backend() -> str: + """AnyIO Backend.""" + return "asyncio" diff --git a/.conflict-side-0/tests/logging/__init__.py b/.conflict-side-0/tests/logging/__init__.py new file mode 100644 index 0000000..a1c677a --- /dev/null +++ b/.conflict-side-0/tests/logging/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Logging Tests.""" diff --git a/.conflict-side-0/tests/logging/test_loguru.py b/.conflict-side-0/tests/logging/test_loguru.py new file mode 100644 index 0000000..9214250 --- /dev/null +++ b/.conflict-side-0/tests/logging/test_loguru.py @@ -0,0 +1,274 @@ +"""Test Logging Loguru.""" + +from collections.abc import Generator +from datetime import datetime +from io import StringIO + +import pytest +from loguru import logger +from pydantic import TypeAdapter + +from grelmicro.errors import DependencyNotFoundError +from grelmicro.logging.errors import LoggingSettingsValidationError +from grelmicro.logging.loguru import ( + JSON_FORMAT, + JSONRecordDict, + configure_logging, + json_formatter, + json_patcher, +) + +json_record_type_adapter = TypeAdapter(JSONRecordDict) + + +@pytest.fixture(autouse=True) +def cleanup_handlers() -> Generator[None, None, None]: + """Cleanup logging handlers.""" + logger.configure(handlers=[]) + yield + logger.remove() + + +def generate_logs() -> int: + """Generate logs.""" + logger.debug("Hello, World!") + logger.info("Hello, World!") + logger.warning("Hello, World!") + logger.error("Hello, Alice!", user="Alice") + try: + 1 / 0 # noqa: B018 + except ZeroDivisionError: + logger.exception("Hello, Bob!") + + return 5 + + +def assert_logs(logs: str) -> None: + """Assert logs.""" + ( + info, + warning, + error, + exception, + ) = ( + json_record_type_adapter.validate_json(line) + for line in logs.splitlines()[0:4] + ) + + expected_separator = 3 + + assert info["logger"] + assert info["logger"].startswith("tests.logging.test_loguru:generate_logs:") + assert len(info["logger"].split(":")) == expected_separator + assert info["time"] == datetime.fromisoformat(info["time"]).isoformat() + assert info["level"] == "INFO" + assert info["msg"] == "Hello, World!" + assert info["thread"] == "MainThread" + assert "ctx" not in info + + assert warning["logger"] + assert warning["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(warning["logger"].split(":")) == expected_separator + assert ( + warning["time"] == datetime.fromisoformat(warning["time"]).isoformat() + ) + assert warning["level"] == "WARNING" + assert warning["msg"] == "Hello, World!" + assert warning["thread"] == "MainThread" + assert "ctx" not in warning + + assert error["logger"] + assert error["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(error["logger"].split(":")) == expected_separator + assert error["time"] == datetime.fromisoformat(error["time"]).isoformat() + assert error["level"] == "ERROR" + assert error["msg"] == "Hello, Alice!" + assert error["thread"] == "MainThread" + assert error["ctx"] == {"user": "Alice"} + + assert exception["logger"] + assert exception["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(exception["logger"].split(":")) == expected_separator + assert ( + exception["time"] + == datetime.fromisoformat(exception["time"]).isoformat() + ) + assert exception["level"] == "ERROR" + assert exception["msg"] == "Hello, Bob!" + assert exception["thread"] == "MainThread" + assert exception["ctx"] == { + "exception": "ZeroDivisionError: division by zero", + } + + +def test_json_formatter() -> None: + """Test JSON Formatter.""" + # Arrange + sink = StringIO() + + # Act + logger.add(sink, format=json_formatter, level="INFO") + generate_logs() + + # Assert + assert_logs(sink.getvalue()) + + +def test_json_patching() -> None: + """Test JSON Patching.""" + # Arrange + sink = StringIO() + + # Act + # logger.patch(json_patcher) -> Patch is not working using logger.configure instead + logger.configure(patcher=json_patcher) + logger.add(sink, format=JSON_FORMAT, level="INFO") + generate_logs() + + # Assert + assert_logs(sink.getvalue()) + + +def test_configure_logging_default( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Default.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + configure_logging() + generate_logs() + + # Assert + assert_logs(capsys.readouterr().out) + + +def test_configure_logging_text( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Text.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "text") + + # Act + configure_logging() + generate_logs() + + # Assert + lines = capsys.readouterr().out.splitlines() + + assert "tests.logging.test_loguru:generate_logs:" in lines[0] + assert " | INFO | " in lines[0] + assert " - Hello, World!" in lines[0] + + assert "tests.logging.test_loguru:generate_logs:" in lines[1] + assert " | WARNING | " in lines[1] + assert " - Hello, World!" in lines[1] + + assert "tests.logging.test_loguru:generate_logs:" in lines[2] + assert " | ERROR | " in lines[2] + assert " - Hello, Alice!" in lines[2] + + assert "tests.logging.test_loguru:generate_logs:" in lines[3] + assert " | ERROR | " in lines[3] + assert " - Hello, Bob!" in lines[3] + assert "Traceback" in lines[4] + assert "ZeroDivisionError: division by zero" in lines[-1] + + +def test_configure_logging_json( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging JSON.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "json") + + # Act + configure_logging() + generate_logs() + + # Assert + assert_logs(capsys.readouterr().out) + + +def test_configure_logging_level( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Level.""" + # Arrange + monkeypatch.setenv("LOG_LEVEL", "DEBUG") + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + configure_logging() + logs_count = generate_logs() + + # Assert + assert len(capsys.readouterr().out.splitlines()) == logs_count + + +def test_configure_logging_invalid_level( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Invalid Level.""" + # Arrange + monkeypatch.setenv("LOG_LEVEL", "INVALID") + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + with pytest.raises( + LoggingSettingsValidationError, + match=( + r"Could not validate environment variables settings:\n" + r"- LOG_LEVEL: Input should be 'DEBUG', 'INFO', 'WARNING', 'ERROR' or 'CRITICAL'" + r" \[input=INVALID\]" + ), + ): + configure_logging() + + # Assert + assert not capsys.readouterr().out + + +def test_configure_logging_format_template( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Format Template.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "{level}: {message}") + + # Act + configure_logging() + generate_logs() + + # Assert + lines = capsys.readouterr().out.splitlines() + assert "INFO: Hello, World!" in lines[0] + assert "WARNING: Hello, World!" in lines[1] + assert "ERROR: Hello, Alice!" in lines[2] + assert "ERROR: Hello, Bob!" in lines[3] + assert "Traceback" in lines[4] + assert "ZeroDivisionError: division by zero" in lines[-1] + + +def test_configure_logging_dependency_not_found( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Configure Logging Dependency Not Found.""" + # Arrange + monkeypatch.setattr("grelmicro.logging.loguru.loguru", None) + + # Act / Assert + with pytest.raises(DependencyNotFoundError, match="loguru"): + configure_logging() diff --git a/.conflict-side-0/tests/sync/__init__.py b/.conflict-side-0/tests/sync/__init__.py new file mode 100644 index 0000000..5e3b5c4 --- /dev/null +++ b/.conflict-side-0/tests/sync/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Synchronization Primitives Tests.""" diff --git a/.conflict-side-0/tests/sync/test_backends.py b/.conflict-side-0/tests/sync/test_backends.py new file mode 100644 index 0000000..b08a92f --- /dev/null +++ b/.conflict-side-0/tests/sync/test_backends.py @@ -0,0 +1,370 @@ +"""Test Synchronization Backends.""" + +from collections.abc import AsyncGenerator, Callable, Generator +from uuid import uuid4 + +import pytest +from anyio import sleep +from testcontainers.core.container import DockerContainer +from testcontainers.postgres import PostgresContainer +from testcontainers.redis import RedisContainer + +from grelmicro.sync._backends import get_sync_backend, loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import BackendNotLoadedError +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.sync.postgres import PostgresSyncBackend +from grelmicro.sync.redis import RedisSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(15)] + + +@pytest.fixture(scope="module") +def anyio_backend() -> str: + """AnyIO Backend Module Scope.""" + return "asyncio" + + +@pytest.fixture(scope="module") +def monkeypatch() -> Generator[pytest.MonkeyPatch, None, None]: + """Monkeypatch Module Scope.""" + monkeypatch = pytest.MonkeyPatch() + yield monkeypatch + monkeypatch.undo() + + +@pytest.fixture +def clean_registry() -> Generator[None, None, None]: + """Make sure the registry is clean.""" + loaded_backends.pop("lock", None) + yield + loaded_backends.pop("lock", None) + + +@pytest.fixture( + params=[ + "memory", + pytest.param("redis", marks=[pytest.mark.integration]), + pytest.param("postgres", marks=[pytest.mark.integration]), + ], + scope="module", +) +def backend_name(request: pytest.FixtureRequest) -> str: + """Backend Name.""" + return request.param + + +@pytest.fixture( + scope="module", +) +def container( + backend_name: str, + monkeypatch: pytest.MonkeyPatch, +) -> Generator[DockerContainer | None, None, None]: + """Test Container for each Backend.""" + if backend_name == "redis": + with RedisContainer() as container: + yield container + elif backend_name == "postgres": + monkeypatch.setenv("POSTGRES_HOST", "localhost") + monkeypatch.setenv("POSTGRES_PORT", "5432") + monkeypatch.setenv("POSTGRES_DB", "test") + monkeypatch.setenv("POSTGRES_USER", "test") + monkeypatch.setenv("POSTGRES_PASSWORD", "test") + with PostgresContainer() as container: + yield container + elif backend_name == "memory": + yield None + + +@pytest.fixture(scope="module") +async def backend( + backend_name: str, container: DockerContainer | None +) -> AsyncGenerator[SyncBackend]: + """Test Container for each Backend.""" + if backend_name == "redis" and container: + port = container.get_exposed_port(6379) + async with RedisSyncBackend(f"redis://localhost:{port}/0") as backend: + yield backend + elif backend_name == "postgres" and container: + port = container.get_exposed_port(5432) + async with PostgresSyncBackend( + f"postgresql://test:test@localhost:{port}/test" + ) as backend: + yield backend + elif backend_name == "memory": + async with MemorySyncBackend() as backend: + yield backend + + +async def test_acquire(backend: SyncBackend) -> None: + """Test acquire.""" + # Arrange + name = "test_acquire" + token = uuid4().hex + duration = 1 + + # Act + result = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result + + +async def test_acquire_reantrant(backend: SyncBackend) -> None: + """Test acquire is reantrant.""" + # Arrange + name = "test_acquire_reantrant" + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result1 + assert result2 + + +async def test_acquire_already_acquired(backend: SyncBackend) -> None: + """Test acquire when already acquired.""" + # Arrange + name = "test_acquire_already_acquired" + token1 = uuid4().hex + token2 = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token1, duration=duration) + result2 = await backend.acquire(name=name, token=token2, duration=duration) + + # Assert + assert token1 != token2 + assert result1 + assert not result2 + + +async def test_acquire_expired(backend: SyncBackend) -> None: + """Test acquire when expired.""" + # Arrange + name = "test_acquire_expired" + token = uuid4().hex + duration = 0.01 + + # Act + result = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result + assert result2 + + +async def test_acquire_already_acquired_expired(backend: SyncBackend) -> None: + """Test acquire when already acquired but expired.""" + # Arrange + name = "test_acquire_already_acquired_expired" + uuid4().hex + token1 = uuid4().hex + token2 = uuid4().hex + duration = 0.01 + + # Act + result = await backend.acquire(name=name, token=token1, duration=duration) + await sleep(duration * 2) + result2 = await backend.acquire(name=name, token=token2, duration=duration) + + # Assert + assert token1 != token2 + assert result + assert result2 + + +async def test_release_not_acquired(backend: SyncBackend) -> None: + """Test release when not acquired.""" + # Arrange + name = "test_release" + uuid4().hex + token = uuid4().hex + + # Act + result = await backend.release(name=name, token=token) + + # Assert + assert not result + + +async def test_release_acquired(backend: SyncBackend) -> None: + """Test release when acquired.""" + # Arrange + name = "test_release_acquired" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert result2 + + +async def test_release_not_reantrant(backend: SyncBackend) -> None: + """Test release is not reantrant.""" + # Arrange + name = "test_release_not_reantrant" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.release(name=name, token=token) + result3 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert result2 + assert not result3 + + +async def test_release_acquired_expired(backend: SyncBackend) -> None: + """Test release when acquired but expired.""" + # Arrange + name = "test_release_acquired_expired" + uuid4().hex + token = uuid4().hex + duration = 0.01 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert not result2 + + +async def test_release_not_acquired_expired(backend: SyncBackend) -> None: + """Test release when not acquired but expired.""" + # Arrange + name = "test_release_not_acquired_expired" + uuid4().hex + token = uuid4().hex + duration = 0.01 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert not result2 + + +async def test_locked(backend: SyncBackend) -> None: + """Test locked.""" + # Arrange + name = "test_locked" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + locked_before = await backend.locked(name=name) + await backend.acquire(name=name, token=token, duration=duration) + locked_after = await backend.locked(name=name) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_owned(backend: SyncBackend) -> None: + """Test owned.""" + # Arrange + name = "test_owned" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + owned_before = await backend.owned(name=name, token=token) + await backend.acquire(name=name, token=token, duration=duration) + owned_after = await backend.owned(name=name, token=token) + + # Assert + assert owned_before is False + assert owned_after is True + + +async def test_owned_another(backend: SyncBackend) -> None: + """Test owned another.""" + # Arrange + name = "test_owned_another" + uuid4().hex + token1 = uuid4().hex + token2 = uuid4().hex + duration = 1 + + # Act + owned_before = await backend.owned(name=name, token=token1) + await backend.acquire(name=name, token=token1, duration=duration) + owned_after = await backend.owned(name=name, token=token2) + + # Assert + assert owned_before is False + assert owned_after is False + + +@pytest.mark.parametrize( + "backend_factory", + [ + lambda: MemorySyncBackend(), + lambda: RedisSyncBackend("redis://localhost:6379/0"), + lambda: PostgresSyncBackend( + "postgresql://user:password@localhost:5432/db" + ), + ], +) +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend(backend_factory: Callable[[], SyncBackend]) -> None: + """Test Get Synchronization Backend.""" + # Arrange + expected_backend = backend_factory() + + # Act + backend = get_sync_backend() + + # Assert + assert backend is expected_backend + + +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend_not_loaded() -> None: + """Test Get Synchronization Backend Not Loaded.""" + # Act / Assert + with pytest.raises(BackendNotLoadedError): + get_sync_backend() + + +@pytest.mark.parametrize( + "backend_factory", + [ + lambda: MemorySyncBackend(auto_register=False), + lambda: RedisSyncBackend( + "redis://localhost:6379/0", auto_register=False + ), + lambda: PostgresSyncBackend( + "postgresql://user:password@localhost:5432/db", auto_register=False + ), + ], +) +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend_auto_register_disabled( + backend_factory: Callable[[], SyncBackend], +) -> None: + """Test Get Synchronization Backend.""" + # Arrange + backend_factory() + + # Act / Assert + with pytest.raises(BackendNotLoadedError): + get_sync_backend() diff --git a/.conflict-side-0/tests/sync/test_leaderelection.py b/.conflict-side-0/tests/sync/test_leaderelection.py new file mode 100644 index 0000000..d357daa --- /dev/null +++ b/.conflict-side-0/tests/sync/test_leaderelection.py @@ -0,0 +1,457 @@ +"""Test leader election.""" + +import math + +import pytest +from anyio import Event, create_task_group, sleep +from pydantic import ValidationError +from pytest_mock import MockerFixture + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.leaderelection import LeaderElection, LeaderElectionConfig +from grelmicro.sync.memory import MemorySyncBackend + +WORKERS = 4 +WORKER_1 = 0 +WORKER_2 = 1 +TEST_TIMEOUT = 1 + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(TEST_TIMEOUT)] + + +@pytest.fixture +def backend() -> SyncBackend: + """Return Memory Synchronization Backend.""" + return MemorySyncBackend() + + +@pytest.fixture +def configs() -> list[LeaderElectionConfig]: + """Leader election Config.""" + return [ + LeaderElectionConfig( + name="test_leader_election", + worker=f"worker_{i}", + lease_duration=0.02, + renew_deadline=0.015, + retry_interval=0.005, + error_interval=0.01, + backend_timeout=0.005, + ) + for i in range(WORKERS) + ] + + +@pytest.fixture +def leader_elections( + backend: SyncBackend, configs: list[LeaderElectionConfig] +) -> list[LeaderElection]: + """Leader elections.""" + return [ + LeaderElection(backend=backend, **configs[i].model_dump()) + for i in range(WORKERS) + ] + + +@pytest.fixture +def leader_election( + backend: SyncBackend, configs: list[LeaderElectionConfig] +) -> LeaderElection: + """Leader election.""" + return LeaderElection(backend=backend, **configs[WORKER_1].model_dump()) + + +async def wait_first_leader(leader_elections: list[LeaderElection]) -> None: + """Wait for the first leader to be elected.""" + + async def wrapper(leader_election: LeaderElection, event: Event) -> None: + """Wait for the leadership.""" + await leader_election.wait_for_leader() + event.set() + + async with create_task_group() as task_group: + event = Event() + for coroutine in leader_elections: + task_group.start_soon(wrapper, coroutine, event) + await event.wait() + task_group.cancel_scope.cancel() + + +def test_leader_election_config() -> None: + """Test leader election Config.""" + # Arrange + config = LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + lease_duration=0.01, + renew_deadline=0.008, + retry_interval=0.001, + error_interval=0.01, + backend_timeout=0.007, + ) + + # Assert + assert config.model_dump() == { + "name": "test_leader_election", + "worker": "worker_1", + "lease_duration": 0.01, + "renew_deadline": 0.008, + "retry_interval": 0.001, + "error_interval": 0.01, + "backend_timeout": 0.007, + } + + +def test_leader_election_config_defaults() -> None: + """Test leader election Config Defaults.""" + # Arrange + config = LeaderElectionConfig( + name="test_leader_election", worker="worker_1" + ) + + # Assert + assert config.model_dump() == { + "name": "test_leader_election", + "worker": "worker_1", + "lease_duration": 15, + "renew_deadline": 10, + "retry_interval": 2, + "error_interval": 30, + "backend_timeout": 5, + } + + +def test_leader_election_config_validation_errors() -> None: + """Test leader election Config Errors.""" + # Arrange + with pytest.raises( + ValidationError, + match="Renew deadline must be shorter than lease duration", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + lease_duration=15, + renew_deadline=20, + ) + with pytest.raises( + ValidationError, + match="Retry interval must be shorter than renew deadline", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + renew_deadline=10, + retry_interval=15, + ) + with pytest.raises( + ValidationError, + match="Backend timeout must be shorter than renew deadline", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + renew_deadline=10, + backend_timeout=15, + ) + + +async def test_lifecycle(leader_election: LeaderElection) -> None: + """Test leader election on worker complete lifecycle.""" + # Act + is_leader_before_start = leader_election.is_leader() + is_running_before_start = leader_election.is_running() + async with create_task_group() as tg: + await tg.start(leader_election) + is_running_after_start = leader_election.is_running() + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + tg.cancel_scope.cancel() + is_running_after_cancel = leader_election.is_running() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_cancel is False + + assert is_running_before_start is False + assert is_running_after_start is True + assert is_running_after_cancel is False + + +async def test_leader_election_context_manager( + leader_election: LeaderElection, +) -> None: + """Test leader election on worker using context manager.""" + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + async with leader_election: + is_leader_inside_context = leader_election.is_leader() + is_leader_after_context = leader_election.is_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_inside_context is True + assert is_leader_after_context is True + assert is_leader_after_cancel is False + + +async def test_leader_election_single_worker( + leader_election: LeaderElection, +) -> None: + """Test leader election on single worker.""" + # Act + async with create_task_group() as tg: + is_leader_before_start = leader_election.is_leader() + await tg.start(leader_election) + is_leader_inside_context = leader_election.is_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_inside_context is True + assert is_leader_after_cancel is False + + +async def test_leadership_abandon_on_renew_deadline_reached( + leader_election: LeaderElection, +) -> None: + """Test leader election abandons leadership when renew deadline is reached.""" + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + leader_election.config.retry_interval = math.inf + await leader_election.wait_lose_leader() + is_leader_after_not_renewed = leader_election.is_leader() + tg.cancel_scope.cancel() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_not_renewed is False + + +async def test_leadership_abandon_on_backend_failure( + leader_election: LeaderElection, + caplog: pytest.LogCaptureFixture, + mocker: MockerFixture, +) -> None: + """Test leader election abandons leadership when backend is unreachable.""" + # Arrange + caplog.set_level("WARNING") + + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + mocker.patch.object( + leader_election.backend, + "acquire", + side_effect=Exception("Backend Unreachable"), + ) + await leader_election.wait_lose_leader() + is_leader_after_not_renewed = leader_election.is_leader() + tg.cancel_scope.cancel() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_not_renewed is False + assert ( + "Leader Election lost leadership: test_leader_election (renew deadline reached)" + in caplog.messages + ) + + +async def test_unepexpected_stop( + leader_election: LeaderElection, mocker: MockerFixture +) -> None: + """Test leader election worker abandons leadership on unexpected stop.""" + + # Arrange + async def leader_election_unexpected_exception() -> None: + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + mock = mocker.patch.object( + leader_election, + "_try_acquire_or_renew", + side_effect=Exception("Unexpected Exception"), + ) + await leader_election.wait_lose_leader() + mock.reset_mock() + tg.cancel_scope.cancel() + + # Act / Assert + with pytest.raises(ExceptionGroup): + await leader_election_unexpected_exception() + + +async def test_release_on_cancel( + backend: SyncBackend, leader_election: LeaderElection, mocker: MockerFixture +) -> None: + """Test leader election on worker that releases the lock on cancel.""" + # Arrange + spy_release = mocker.spy(backend, "release") + + # Act + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + + # Assert + spy_release.assert_called_once() + + +async def test_release_failure_ignored( + backend: SyncBackend, + leader_election: LeaderElection, + mocker: MockerFixture, +) -> None: + """Test leader election on worker that ignores release failure.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Unreachable") + ) + + # Act + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + + +async def test_only_one_leader(leader_elections: list[LeaderElection]) -> None: + """Test leader election on multiple workers ensuring only one leader is elected.""" + # Act + leaders_before_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + async with create_task_group() as tg: + for leader_election in leader_elections: + await tg.start(leader_election) + await wait_first_leader(leader_elections) + leaders_after_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + tg.cancel_scope.cancel() + for leader_election in leader_elections: + await leader_election.wait_lose_leader() + leaders_after_cancel = [ + leader_election.is_leader() for leader_election in leader_elections + ] + + # Assert + assert sum(leaders_before_start) == 0 + assert sum(leaders_after_start) == 1 + assert sum(leaders_after_cancel) == 0 + + +async def test_leader_transition( + leader_elections: list[LeaderElection], +) -> None: + """Test leader election leader transition to another worker.""" + # Arrange + leaders_after_leader_election1_start = [False] * len(leader_elections) + leaders_after_all_start = [False] * len(leader_elections) + leaders_after_leader_election1_down = [False] * len(leader_elections) + + # Act + leaders_before_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + async with create_task_group() as workers_tg: + async with create_task_group() as worker1_tg: + await worker1_tg.start(leader_elections[WORKER_1]) + await leader_elections[WORKER_1].wait_for_leader() + leaders_after_leader_election1_start = [ + leader_election.is_leader() + for leader_election in leader_elections + ] + + for leader_election in leader_elections: + await workers_tg.start(leader_election) + leaders_after_all_start = [ + leader_election.is_leader() + for leader_election in leader_elections + ] + worker1_tg.cancel_scope.cancel() + + await leader_elections[WORKER_1].wait_lose_leader() + + await wait_first_leader(leader_elections) + leaders_after_leader_election1_down = [ + leader_election.is_leader() for leader_election in leader_elections + ] + workers_tg.cancel_scope.cancel() + + for leader_election in leader_elections[WORKER_2:]: + await leader_election.wait_lose_leader() + leaders_after_all_down = [ + leader_election.is_leader() for leader_election in leader_elections + ] + + # Assert + assert sum(leaders_before_start) == 0 + assert sum(leaders_after_leader_election1_start) == 1 + assert sum(leaders_after_all_start) == 1 + assert sum(leaders_after_leader_election1_down) == 1 + assert sum(leaders_after_all_down) == 0 + + assert leaders_after_leader_election1_start[WORKER_1] is True + assert leaders_after_leader_election1_down[WORKER_1] is False + + +async def test_error_interval( + backend: SyncBackend, + leader_elections: list[LeaderElection], + caplog: pytest.LogCaptureFixture, + mocker: MockerFixture, +) -> None: + """Test leader election on worker with error cooldown.""" + # Arrange + caplog.set_level("ERROR") + leader_elections[WORKER_1].config.error_interval = 1 + leader_elections[WORKER_2].config.error_interval = 0.001 + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Unreachable") + ) + + # Act + async with create_task_group() as tg: + await tg.start(leader_elections[WORKER_1]) + await sleep(0.01) + tg.cancel_scope.cancel() + leader_election1_nb_errors = sum( + 1 for record in caplog.records if record.levelname == "ERROR" + ) + caplog.clear() + + async with create_task_group() as tg: + await tg.start(leader_elections[WORKER_2]) + await sleep(0.01) + tg.cancel_scope.cancel() + leader_election2_nb_errors = sum( + 1 for record in caplog.records if record.levelname == "ERROR" + ) + + # Assert + assert leader_election1_nb_errors == 1 + assert leader_election2_nb_errors >= 1 diff --git a/.conflict-side-0/tests/sync/test_lock.py b/.conflict-side-0/tests/sync/test_lock.py new file mode 100644 index 0000000..42e0b04 --- /dev/null +++ b/.conflict-side-0/tests/sync/test_lock.py @@ -0,0 +1,506 @@ +"""Test Lock.""" + +import time +from collections.abc import AsyncGenerator + +import pytest +from anyio import WouldBlock, sleep, to_thread +from pytest_mock import MockerFixture + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import ( + LockAcquireError, + LockNotOwnedError, + LockReleaseError, + SyncBackendError, +) +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +WORKER_1 = 0 +WORKER_2 = 1 +WORKER_COUNT = 2 + +LOCK_NAME = "test_leased_lock" + + +@pytest.fixture +async def backend() -> AsyncGenerator[SyncBackend]: + """Return Memory Synchronization Backend.""" + async with MemorySyncBackend() as backend: + yield backend + + +@pytest.fixture +def locks(backend: SyncBackend) -> list[Lock]: + """Locks of multiple workers.""" + return [ + Lock( + backend=backend, + name=LOCK_NAME, + worker=f"worker_{i}", + lease_duration=0.01, + retry_interval=0.001, + ) + for i in range(WORKER_COUNT) + ] + + +@pytest.fixture +def lock(locks: list[Lock]) -> Lock: + """Lock.""" + return locks[WORKER_1] + + +async def test_lock_owned(locks: list[Lock]) -> None: + """Test Lock owned.""" + # Act + worker_1_owned_before = await locks[WORKER_1].owned() + worker_2_owned_before = await locks[WORKER_2].owned() + await locks[WORKER_1].acquire() + worker_1_owned_after = await locks[WORKER_1].owned() + worker_2_owned_after = await locks[WORKER_2].owned() + + # Assert + assert worker_1_owned_before is False + assert worker_2_owned_before is False + assert worker_1_owned_after is True + assert worker_2_owned_after is False + + +async def test_lock_from_thread_owned(locks: list[Lock]) -> None: + """Test Lock from thread owned.""" + # Arrange + worker_1_owned_before = None + worker_2_owned_before = None + worker_1_owned_after = None + worker_2_owned_after = None + + # Act + def sync() -> None: + nonlocal worker_1_owned_before + nonlocal worker_2_owned_before + nonlocal worker_1_owned_after + nonlocal worker_2_owned_after + + worker_1_owned_before = locks[WORKER_1].from_thread.owned() + worker_2_owned_before = locks[WORKER_2].from_thread.owned() + locks[WORKER_1].from_thread.acquire() + worker_1_owned_after = locks[WORKER_1].from_thread.owned() + worker_2_owned_after = locks[WORKER_2].from_thread.owned() + + await to_thread.run_sync(sync) + + # Assert + assert worker_1_owned_before is False + assert worker_2_owned_before is False + assert worker_1_owned_after is True + assert worker_2_owned_after is False + + +async def test_lock_context_manager(lock: Lock) -> None: + """Test Lock context manager.""" + # Act + locked_before = await lock.locked() + async with lock: + locked_inside = await lock.locked() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_inside is True + assert locked_after is False + + +async def test_lock_from_thread_context_manager_acquire(lock: Lock) -> None: + """Test Lock from thread context manager.""" + # Arrange + locked_before = None + locked_inside = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_inside + nonlocal locked_after + + locked_before = lock.from_thread.locked() + with lock.from_thread: + locked_inside = lock.from_thread.locked() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_inside is True + assert locked_after is False + + +async def test_lock_context_manager_wait(lock: Lock, locks: list[Lock]) -> None: + """Test Lock context manager wait.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act + locked_before = await lock.locked() + async with locks[WORKER_2]: # Wait until lock expires + locked_inside = await lock.locked() + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_inside is True + assert locked_after is False + + +async def test_lock_from_thread_context_manager_wait( + lock: Lock, locks: list[Lock] +) -> None: + """Test Lock from thread context manager wait.""" + # Arrange + locked_before = None + locked_inside = None + locked_after = None + await locks[WORKER_1].acquire() + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_inside + nonlocal locked_after + + locked_before = lock.from_thread.locked() + with locks[WORKER_2].from_thread: + locked_inside = lock.from_thread.locked() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is True + assert locked_inside is True + assert locked_after is False + + +async def test_lock_acquire(lock: Lock) -> None: + """Test Lock acquire.""" + # Act + locked_before = await lock.locked() + await lock.acquire() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_from_thread_acquire(lock: Lock) -> None: + """Test Lock from thread acquire.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_wait(lock: Lock, locks: list[Lock]) -> None: + """Test Lock acquire wait.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act + locked_before = await lock.locked() + await locks[WORKER_2].acquire() # Wait until lock expires + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_after is True + + +async def test_lock_from_thread_acquire_wait(lock: Lock) -> None: + """Test Lock from thread acquire wait.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_nowait(lock: Lock) -> None: + """Test Lock wait acquire.""" + # Act + locked_before = await lock.locked() + await lock.acquire_nowait() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_from_thread_acquire_nowait(lock: Lock) -> None: + """Test Lock from thread wait acquire.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire_nowait() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_nowait_would_block(locks: list[Lock]) -> None: + """Test Lock wait acquire would block.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act / Assert + with pytest.raises(WouldBlock): + await locks[WORKER_2].acquire_nowait() + + +async def test_lock_from_thread_acquire_nowait_would_block( + locks: list[Lock], +) -> None: + """Test Lock from thread wait acquire would block.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act / Assert + def sync() -> None: + with pytest.raises(WouldBlock): + locks[WORKER_2].from_thread.acquire_nowait() + + await to_thread.run_sync(sync) + + +async def test_lock_release(lock: Lock) -> None: + """Test Lock release.""" + # Act / Assert + with pytest.raises(LockNotOwnedError): + await lock.release() + + +async def test_lock_from_thread_release(lock: Lock) -> None: + """Test Lock from thread release.""" + + # Act / Assert + def sync() -> None: + with pytest.raises(LockNotOwnedError): + lock.from_thread.release() + + await to_thread.run_sync(sync) + + +async def test_lock_release_acquired(lock: Lock) -> None: + """Test Lock release acquired.""" + # Arrange + await lock.acquire() + + # Act + locked_before = await lock.locked() + await lock.release() + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_after is False + + +async def test_lock_from_thread_release_acquired(lock: Lock) -> None: + """Test Lock from thread release acquired.""" + # Arrange + locked_before = None + locked_after = None + + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + lock.from_thread.acquire() + + # Act + locked_before = lock.from_thread.locked() + lock.from_thread.release() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is True + assert locked_after is False + + +async def test_lock_release_expired(locks: list[Lock]) -> None: + """Test Lock release expired.""" + # Arrange + await locks[WORKER_1].acquire() + await sleep(locks[WORKER_1].config.lease_duration) + + # Act + worker_1_locked_before = await locks[WORKER_1].locked() + with pytest.raises(LockNotOwnedError): + await locks[WORKER_2].release() + + # Assert + assert worker_1_locked_before is False + + +async def test_lock_from_thread_release_expired(locks: list[Lock]) -> None: + """Test Lock from thread release expired.""" + # Arrange + worker_1_locked_before = None + + def sync() -> None: + nonlocal worker_1_locked_before + + locks[WORKER_1].from_thread.acquire() + time.sleep(locks[WORKER_1].config.lease_duration) + + # Act + worker_1_locked_before = locks[WORKER_1].from_thread.locked() + with pytest.raises(LockNotOwnedError): + locks[WORKER_2].from_thread.release() + + await to_thread.run_sync(sync) + + # Assert + assert worker_1_locked_before is False + + +async def test_lock_acquire_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock acquire backend error.""" + # Arrange + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Error") + ) + + # Act + with pytest.raises(LockAcquireError): + await lock.acquire() + + +async def test_lock_from_thread_acquire_backend_error( + backend: SyncBackend, + lock: Lock, + mocker: MockerFixture, +) -> None: + """Test Lock from thread acquire backend error.""" + # Arrange + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Error") + ) + + # Act + def sync() -> None: + with pytest.raises(LockAcquireError): + lock.from_thread.acquire() + + await to_thread.run_sync(sync) + + +async def test_lock_release_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock release backend error.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Error") + ) + + # Act + await lock.acquire() + with pytest.raises(LockReleaseError): + await lock.release() + + +async def test_lock_from_thread_release_backend_error( + backend: SyncBackend, + lock: Lock, + mocker: MockerFixture, +) -> None: + """Test Lock from thread release backend error.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Error") + ) + + # Act + def sync() -> None: + lock.from_thread.acquire() + with pytest.raises(LockReleaseError): + lock.from_thread.release() + + await to_thread.run_sync(sync) + + +async def test_lock_owned_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock owned backend error.""" + # Arrange + mocker.patch.object( + backend, "owned", side_effect=Exception("Backend Error") + ) + + # Act / Assert + with pytest.raises(SyncBackendError): + await lock.owned() + + +async def test_lock_locked_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock locked backend error.""" + # Arrange + mocker.patch.object( + backend, "locked", side_effect=Exception("Backend Error") + ) + + # Act / Assert + with pytest.raises(SyncBackendError): + await lock.locked() diff --git a/.conflict-side-0/tests/sync/test_postgres.py b/.conflict-side-0/tests/sync/test_postgres.py new file mode 100644 index 0000000..ef8dd18 --- /dev/null +++ b/.conflict-side-0/tests/sync/test_postgres.py @@ -0,0 +1,106 @@ +"""Tests for PostgreSQL Backends.""" + +import pytest + +from grelmicro.errors import OutOfContextError +from grelmicro.sync.errors import SyncSettingsValidationError +from grelmicro.sync.postgres import PostgresSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +URL = "postgres://user:password@localhost:5432/db" + + +@pytest.mark.parametrize( + "table_name", + [ + "locks table", + "%locks", + "locks;table", + "locks' OR '1'='1", + "locks; DROP TABLE users; --", + ], +) +def test_sync_backend_table_name_invalid(table_name: str) -> None: + """Test Synchronization Backend Table Name Invalid.""" + # Act / Assert + with pytest.raises( + ValueError, match="Table name '.*' is not a valid identifier" + ): + PostgresSyncBackend(url=URL, table_name=table_name) + + +async def test_sync_backend_out_of_context_errors() -> None: + """Test Synchronization Backend Out Of Context Errors.""" + # Arrange + backend = PostgresSyncBackend(url=URL) + name = "lock" + key = "token" + + # Act / Assert + with pytest.raises(OutOfContextError): + await backend.acquire(name=name, token=key, duration=1) + with pytest.raises(OutOfContextError): + await backend.release(name=name, token=key) + with pytest.raises(OutOfContextError): + await backend.locked(name=name) + with pytest.raises(OutOfContextError): + await backend.owned(name=name, token=key) + + +@pytest.mark.parametrize( + ("environs"), + [ + { + "POSTGRES_URL": "postgresql://test_user:test_password@test_host:1234/test_db" + }, + { + "POSTGRES_USER": "test_user", + "POSTGRES_PASSWORD": "test_password", + "POSTGRES_HOST": "test_host", + "POSTGRES_PORT": "1234", + "POSTGRES_DB": "test_db", + }, + ], +) +def test_postgres_env_var_settings( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test PostgreSQL Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Act + backend = PostgresSyncBackend() + + # Assert + assert ( + backend._url + == "postgresql://test_user:test_password@test_host:1234/test_db" + ) + + +@pytest.mark.parametrize( + ("environs"), + [ + { + "POSTGRES_URL": "test://test_user:test_password@test_host:1234/test_db" + }, + {"POSTGRES_USER": "test_user"}, + ], +) +def test_postgres_env_var_settings_validation_error( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test PostgreSQL Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Assert / Act + with pytest.raises( + SyncSettingsValidationError, + match=(r"Could not validate environment variables settings:\n"), + ): + PostgresSyncBackend() diff --git a/.conflict-side-0/tests/sync/test_redis.py b/.conflict-side-0/tests/sync/test_redis.py new file mode 100644 index 0000000..a14bad7 --- /dev/null +++ b/.conflict-side-0/tests/sync/test_redis.py @@ -0,0 +1,67 @@ +"""Tests for Redis Backends.""" + +import pytest + +from grelmicro.sync.errors import SyncSettingsValidationError +from grelmicro.sync.redis import RedisSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +URL = "redis://:test_password@test_host:1234/0" + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": URL}, + { + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Act + backend = RedisSyncBackend() + + # Assert + assert backend._url == URL + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": "test://:test_password@test_host:1234/0"}, + {"REDIS_PASSWORD": "test_password"}, + { + "REDIS_URL": "test://:test_password@test_host:1234/0", + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings_validation_error( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Assert / Act + with pytest.raises( + SyncSettingsValidationError, + match=(r"Could not validate environment variables settings:\n"), + ): + RedisSyncBackend() diff --git a/.conflict-side-0/tests/sync/utils.py b/.conflict-side-0/tests/sync/utils.py new file mode 100644 index 0000000..e20356b --- /dev/null +++ b/.conflict-side-0/tests/sync/utils.py @@ -0,0 +1,23 @@ +"""Test utilities for Lock.""" + +from anyio import Event, create_task_group, fail_after + +from grelmicro.sync._base import BaseLock + + +async def wait_first_acquired(locks: list[BaseLock]) -> None: + """Wait for the first lock to be acquired.""" + + async def wrapper(lock: BaseLock, event: Event) -> None: + """Send event when lock is acquired.""" + with fail_after(1): + await lock.acquire() + event.set() + + with fail_after(1): + async with create_task_group() as task_group: + event = Event() + for lock in locks: + task_group.start_soon(wrapper, lock, event) + await event.wait() + task_group.cancel_scope.cancel() diff --git a/.conflict-side-0/tests/task/__init__.py b/.conflict-side-0/tests/task/__init__.py new file mode 100644 index 0000000..ebf85b3 --- /dev/null +++ b/.conflict-side-0/tests/task/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Task Scheduler Tests.""" diff --git a/.conflict-side-0/tests/task/samples.py b/.conflict-side-0/tests/task/samples.py new file mode 100644 index 0000000..d19c153 --- /dev/null +++ b/.conflict-side-0/tests/task/samples.py @@ -0,0 +1,86 @@ +"""Test Samples for the Task Component.""" + +from types import TracebackType +from typing import Self + +from anyio import TASK_STATUS_IGNORED, Condition, Event +from anyio.abc import TaskStatus +from typer import echo + +from grelmicro.sync.abc import Synchronization +from grelmicro.task.abc import Task + +condition = Condition() + + +def test1() -> None: + """Test Function.""" + echo("test1") + + +def test2() -> None: + """Test Function.""" + + +def test3(test: str = "test") -> None: + """Test Function.""" + + +async def notify() -> None: + """Test Function that notifies the condition.""" + async with condition: + condition.notify() + + +async def always_fail() -> None: + """Test Function that always fails.""" + msg = "Test Error" + raise ValueError(msg) + + +class SimpleClass: + """Test Class.""" + + def method(self) -> None: + """Test Method.""" + + @staticmethod + def static_method() -> None: + """Test Static Method.""" + + +class EventTask(Task): + """Test Scheduled Task with Event.""" + + def __init__(self, *, event: Event | None = None) -> None: + """Initialize the event task.""" + self._event = event or Event() + + @property + def name(self) -> str: + """Return the task name.""" + return "event_task" + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run the task that sets the event.""" + task_status.started() + self._event.set() + + +class BadLock(Synchronization): + """Bad Lock.""" + + async def __aenter__(self) -> Self: + """Enter the synchronization primitive.""" + msg = "Bad Lock" + raise ValueError(msg) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the synchronization primitive.""" diff --git a/.conflict-side-0/tests/task/test_interval.py b/.conflict-side-0/tests/task/test_interval.py new file mode 100644 index 0000000..308d456 --- /dev/null +++ b/.conflict-side-0/tests/task/test_interval.py @@ -0,0 +1,127 @@ +"""Test Interval Task.""" + +import pytest +from anyio import create_task_group, sleep, sleep_forever +from pytest_mock import MockFixture + +from grelmicro.task._interval import IntervalTask +from tests.task.samples import ( + BadLock, + always_fail, + condition, + notify, + test1, +) + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] + +INTERVAL = 0.1 +SLEEP = 0.01 + + +def test_interval_task_init() -> None: + """Test Interval Task Initialization.""" + # Act + task = IntervalTask(interval=1, function=test1) + # Assert + assert task.name == "tests.task.samples:test1" + + +def test_interval_task_init_with_name() -> None: + """Test Interval Task Initialization with Name.""" + # Act + task = IntervalTask(interval=1, function=test1, name="test1") + # Assert + assert task.name == "test1" + + +def test_interval_task_init_with_invalid_interval() -> None: + """Test Interval Task Initialization with Invalid Interval.""" + # Act / Assert + with pytest.raises(ValueError, match="Interval must be greater than 0"): + IntervalTask(interval=0, function=test1) + + +async def test_interval_task_start() -> None: + """Test Interval Task Start.""" + # Arrange + task = IntervalTask(interval=1, function=notify) + # Act + async with create_task_group() as tg: + await tg.start(task) + async with condition: + await condition.wait() + tg.cancel_scope.cancel() + + +async def test_interval_task_execution_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test Interval Task Execution Error.""" + # Arrange + task = IntervalTask(interval=1, function=always_fail) + # Act + async with create_task_group() as tg: + await tg.start(task) + await sleep(SLEEP) + tg.cancel_scope.cancel() + + # Assert + assert any( + "Task execution error:" in record.message + for record in caplog.records + if record.levelname == "ERROR" + ) + + +async def test_interval_task_synchronization_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test Interval Task Synchronization Error.""" + # Arrange + task = IntervalTask(interval=1, function=notify, sync=BadLock()) + + # Act + async with create_task_group() as tg: + await tg.start(task) + await sleep(SLEEP) + tg.cancel_scope.cancel() + + # Assert + assert any( + "Task synchronization error:" in record.message + for record in caplog.records + if record.levelname == "ERROR" + ) + + +async def test_interval_stop( + caplog: pytest.LogCaptureFixture, mocker: MockFixture +) -> None: + """Test Interval Task stop.""" + # Arrange + caplog.set_level("INFO") + + class CustomBaseException(BaseException): + pass + + mocker.patch( + "grelmicro.task._interval.sleep", side_effect=CustomBaseException + ) + task = IntervalTask(interval=1, function=test1) + + async def leader_election_during_runtime_error() -> None: + async with create_task_group() as tg: + await tg.start(task) + await sleep_forever() + + # Act + with pytest.raises(BaseExceptionGroup): + await leader_election_during_runtime_error() + + # Assert + assert any( + "Task stopped:" in record.message + for record in caplog.records + if record.levelname == "INFO" + ) diff --git a/.conflict-side-0/tests/task/test_manager.py b/.conflict-side-0/tests/task/test_manager.py new file mode 100644 index 0000000..62c9859 --- /dev/null +++ b/.conflict-side-0/tests/task/test_manager.py @@ -0,0 +1,81 @@ +"""Test Task Manager.""" + +import pytest +from anyio import Event + +from grelmicro.errors import OutOfContextError +from grelmicro.task import TaskManager +from grelmicro.task.errors import TaskAddOperationError +from tests.task.samples import EventTask + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] + + +def test_task_manager_init() -> None: + """Test Task Manager Initialization.""" + # Act + task = EventTask() + app = TaskManager() + app_with_tasks = TaskManager(tasks=[task]) + # Assert + assert app.tasks == [] + assert app_with_tasks.tasks == [task] + + +async def test_task_manager_context() -> None: + """Test Task Manager Context.""" + # Arrange + event = Event() + task = EventTask(event=event) + app = TaskManager(tasks=[task]) + + # Act + event_before = event.is_set() + async with app: + event_in_context = event.is_set() + + # Assert + assert event_before is False + assert event_in_context is True + + +@pytest.mark.parametrize("auto_start", [True, False]) +async def test_task_manager_auto_start_disabled(*, auto_start: bool) -> None: + """Test Task Manager Auto Start Disabled.""" + # Arrange + event = Event() + task = EventTask(event=event) + app = TaskManager(auto_start=auto_start, tasks=[task]) + + # Act + event_before = event.is_set() + async with app: + event_in_context = event.is_set() + + # Assert + assert event_before is False + assert event_in_context is auto_start + + +async def test_task_manager_already_started_error() -> None: + """Test Task Manager Already Started Warning.""" + # Arrange + app = TaskManager() + + # Act / Assert + async with app: + with pytest.raises(TaskAddOperationError): + await app.start() + + +async def test_task_manager_out_of_context_errors() -> None: + """Test Task Manager Out of Context Errors.""" + # Arrange + app = TaskManager() + + # Act / Assert + with pytest.raises(OutOfContextError): + await app.start() + + with pytest.raises(OutOfContextError): + await app.__aexit__(None, None, None) diff --git a/.conflict-side-0/tests/task/test_router.py b/.conflict-side-0/tests/task/test_router.py new file mode 100644 index 0000000..ed30af7 --- /dev/null +++ b/.conflict-side-0/tests/task/test_router.py @@ -0,0 +1,175 @@ +"""Test Task Router.""" + +from functools import partial + +import pytest + +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.task import TaskRouter +from grelmicro.task._interval import IntervalTask +from grelmicro.task.errors import FunctionTypeError, TaskAddOperationError +from tests.task.samples import EventTask, SimpleClass, test1, test2, test3 + + +def test_router_init() -> None: + """Test Task Router Initialization.""" + # Arrange + custom_task = EventTask() + + # Act + router = TaskRouter() + router_with_task = TaskRouter(tasks=[custom_task]) + + # Assert + assert router.tasks == [] + assert router_with_task.tasks == [custom_task] + + +def test_router_add_task() -> None: + """Test Task Router Add Task.""" + # Arrange + custom_task1 = EventTask() + custom_task2 = EventTask() + router = TaskRouter() + router_with_task = TaskRouter(tasks=[custom_task1]) + + # Act + router.add_task(custom_task1) + router_with_task.add_task(custom_task2) + + # Assert + assert router.tasks == [custom_task1] + assert router_with_task.tasks == [custom_task1, custom_task2] + + +def test_router_include_router() -> None: + """Test Task Router Include Router.""" + # Arrange + custom_task1 = EventTask() + custom_task2 = EventTask() + router = TaskRouter(tasks=[custom_task1]) + router_with_task = TaskRouter(tasks=[custom_task2]) + + # Act + router.include_router(router_with_task) + + # Assert + assert router.tasks == [custom_task1, custom_task2] + + +def test_router_interval() -> None: + """Test Task Router add interval task.""" + # Arrange + task_count = 4 + custom_task = EventTask() + router = TaskRouter(tasks=[custom_task]) + sync = Lock(backend=MemorySyncBackend(), name="testlock") + + # Act + router.interval(name="test1", seconds=10, sync=sync)(test1) + router.interval(name="test2", seconds=20)(test2) + router.interval(seconds=10)(test3) + + # Assert + assert len(router.tasks) == task_count + assert ( + sum(isinstance(task, IntervalTask) for task in router.tasks) + == task_count - 1 + ) + assert router.tasks[0].name == "event_task" + assert router.tasks[1].name == "test1" + assert router.tasks[2].name == "test2" + assert router.tasks[3].name == "tests.task.samples:test3" + + +def test_router_interval_name_generation() -> None: + """Test Task Router Interval Name Generation.""" + # Arrange + router = TaskRouter() + + # Act + router.interval(seconds=10)(test1) + router.interval(seconds=10)(SimpleClass.static_method) + router.interval(seconds=10)(SimpleClass.method) + + # Assert + assert router.tasks[0].name == "tests.task.samples:test1" + assert ( + router.tasks[1].name == "tests.task.samples:SimpleClass.static_method" + ) + assert router.tasks[2].name == "tests.task.samples:SimpleClass.method" + + +def test_router_interval_name_generation_error() -> None: + """Test Task Router Interval Name Generation Error.""" + # Arrange + router = TaskRouter() + test_instance = SimpleClass() + + # Act + with pytest.raises(FunctionTypeError, match="nested function"): + + @router.interval(seconds=10) + def nested_function() -> None: + pass + + with pytest.raises(FunctionTypeError, match="lambda"): + router.interval(seconds=10)(lambda _: None) + + with pytest.raises(FunctionTypeError, match="method"): + router.interval(seconds=10)(test_instance.method) + + with pytest.raises(FunctionTypeError, match="partial()"): + router.interval(seconds=10)(partial(test1)) + + with pytest.raises( + FunctionTypeError, + match="callable without __module__ or __qualname__ attribute", + ): + router.interval(seconds=10)(object()) # type: ignore[arg-type] + + +def test_router_add_task_when_started() -> None: + """Test Task Router Add Task When Started.""" + # Arrange + custom_task = EventTask() + router = TaskRouter() + router.do_mark_as_started() + + # Act + with pytest.raises(TaskAddOperationError): + router.add_task(custom_task) + + +def test_router_include_router_when_started() -> None: + """Test Task Router Include Router When Started.""" + # Arrange + router = TaskRouter() + router.do_mark_as_started() + router_child = TaskRouter() + + # Act + with pytest.raises(TaskAddOperationError): + router.include_router(router_child) + + +def test_router_started_propagation() -> None: + """Test Task Router Started Propagation.""" + # Arrange + router = TaskRouter() + router_child = TaskRouter() + router.include_router(router_child) + + # Act + router_started_before = router.started() + router_child_started_before = router_child.started() + router.do_mark_as_started() + router_started_after = router.started() + router_child_started_after = router_child.started() + + # Assert + assert router_started_before is False + assert router_child_started_before is False + assert router_started_after is True + assert router_child_started_after is True diff --git a/.conflict-side-0/uv.lock b/.conflict-side-0/uv.lock new file mode 100644 index 0000000..ff11a2b --- /dev/null +++ b/.conflict-side-0/uv.lock @@ -0,0 +1,1934 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version < '3.13'", + "python_full_version >= '3.13'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, + { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, + { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, + { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, + { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, + { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, + { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, + { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 }, + { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 }, + { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 }, + { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 }, + { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 }, + { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 }, + { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 }, + { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 }, + { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 }, + { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 }, + { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 }, + { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 }, + { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 }, + { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 }, + { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 }, + { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 }, + { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, + { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, + { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, + { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, + { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, + { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, + { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, + { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, + { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, + { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, + { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, + { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, + { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, + { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, + { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, + { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, + { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, + { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, + { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, + { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, + { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, + { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, + { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "43.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, + { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, + { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, + { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, + { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, + { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, + { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, + { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, + { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, + { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, + { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, + { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, +] + +[[package]] +name = "cyclic" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, +] + +[[package]] +name = "fast-depends" +version = "2.4.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/f5/8b42b7588a67ad78991e5e7ca0e0c6a1ded535a69a725e4e48d3346a20c1/fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d", size = 16682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/08/4adb160d8394053289fdf3b276e93b53271fd463e54fff8911b23c1db4ed/fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2", size = 17651 }, +] + +[[package]] +name = "fastapi" +version = "0.115.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/29/f71316b9273b6552a263748e49cd7b83898dc9499a663d30c7b9cb853cb8/fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", size = 301047 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c4/148d5046a96c428464557264877ae5a9338a83bbe0df045088749ec89820/fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796", size = 94866 }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/1ad5ce32d029aeb9117e9a5a9b3e314a8477525d60c12a9b7730a3c186ec/fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", size = 15571 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/ea/4b5011012ac925fe2f83b19d0e09cee9d324141ec7bf5e78bb2817f96513/fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46", size = 9489 }, +] + +[[package]] +name = "faststream" +version = "0.5.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "fast-depends" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/d3/c2a3e1233274c93a4978cbac210a81ba05cee09e2e0051049b40f55406f1/faststream-0.5.30.tar.gz", hash = "sha256:50ad5288719cfa75c13e9c277d40afae62533a590facad6e6d215e868f2b97f4", size = 284478 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/ce/a9eec6c2c9803de6bc2b2a5cac35d56b8908c64fcdd4c73616c1a16c9b90/faststream-0.5.30-py3-none-any.whl", hash = "sha256:bf48826be99210f3e9c7dff1b2a17b4bc4762c873c5558ac81b9b873549ae6a1", size = 382011 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "grelmicro" +version = "0.2.2" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "fast-depends" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, +] + +[package.optional-dependencies] +postgres = [ + { name = "asyncpg" }, +] +redis = [ + { name = "redis" }, +] +standard = [ + { name = "loguru" }, + { name = "orjson" }, +] + +[package.dev-dependencies] +dev = [ + { name = "fastapi" }, + { name = "fastapi-cli" }, + { name = "faststream" }, + { name = "hatch" }, + { name = "mdx-include" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-randomly" }, + { name = "pytest-timeout" }, + { name = "ruff" }, + { name = "testcontainers", extra = ["redis"] }, +] +docs = [ + { name = "mkdocs-material" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.0.0" }, + { name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.30.0" }, + { name = "fast-depends", specifier = ">=2.0.0" }, + { name = "loguru", marker = "extra == 'standard'", specifier = ">=0.7.2" }, + { name = "orjson", marker = "extra == 'standard'", specifier = ">=3.10.11" }, + { name = "pydantic", specifier = ">=2.5.0" }, + { name = "pydantic-settings", specifier = ">=2.5.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "fastapi", specifier = ">=0.115.5" }, + { name = "fastapi-cli", specifier = ">=0.0.5" }, + { name = "faststream", specifier = ">=0.5.30" }, + { name = "hatch", specifier = ">=1.13.0" }, + { name = "mdx-include", specifier = ">=1.4.2" }, + { name = "mypy", specifier = ">=1.12.0" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-mock", specifier = ">=3.14.0" }, + { name = "pytest-randomly", specifier = ">=3.16.0" }, + { name = "pytest-timeout", specifier = ">=2.3.1" }, + { name = "ruff", specifier = ">=0.7.4" }, + { name = "testcontainers", extras = ["postgres", "redis"], specifier = ">=4.8.2" }, +] +docs = [ + { name = "mkdocs-material", specifier = ">=9.5.44" }, + { name = "pygments", specifier = ">=2.18.0" }, + { name = "pymdown-extensions", specifier = ">=10.12" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "hatch" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "hyperlink" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "pexpect" }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "userpath" }, + { name = "uv" }, + { name = "virtualenv" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/ed/5001de278f8d7381cbc84f5efdae72308fe37493bc063878f6a1ac07dab8/hatch-1.13.0.tar.gz", hash = "sha256:5e1a75770cfe8f3ebae3abfded3a976238b0acefd19cdabc5245597525b8066f", size = 5188060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/8d/6d965a22bc38cec091ba82131624bb5d75471094d7fe05e829536de3de2f/hatch-1.13.0-py3-none-any.whl", hash = "sha256:bb1a18558a626279cae338b4d8a9d3ca4226d5e06d50de600608c57acd131b67", size = 125757 }, +] + +[[package]] +name = "hatchling" +version = "1.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/47/7ec270a9567262ae3cb32dd420d2b53bf7aee769aca1f240eae0426b5bbc/hatchling-1.26.3.tar.gz", hash = "sha256:b672a9c36a601a06c4e88a1abb1330639ee8e721e0535a37536e546a667efc7a", size = 54968 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl", hash = "sha256:c407e1c6c17b574584a66ae60e8e9a01235ecb6dc61d01559bb936577aaf5846", size = 75773 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, +] + +[[package]] +name = "identify" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/79/7a520fc5011e02ca3f3285b5f6820eaf80443eb73e3733f73c02fb42ba0b/identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd", size = 99113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/86/c4395700f3c5475424fb5c41e20c16be28d10c904aee4d005ba3217fc8e7/identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", size = 98982 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, +] + +[[package]] +name = "jeepney" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "keyring" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/24/64447b13df6a0e2797b586dad715766d756c932ce8ace7f67bd384d76ae0/keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6", size = 62675 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c9/353c156fa2f057e669106e5d6bcdecf85ef8d3536ce68ca96f18dc7b6d6f/keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741", size = 39096 }, +] + +[[package]] +name = "loguru" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549 }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mdx-include" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cyclic" }, + { name = "markdown" }, + { name = "rcslice" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591 }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.5.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/56/182d8121db9ab553cdf9bc58d5972b89833f60b63272f693c1f2b849b640/mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0", size = 3964306 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/eb/a801d00e0e210d82184aacce596906ec065422c78a7319244ba0771c4ded/mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca", size = 8674509 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, +] + +[[package]] +name = "mypy" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/03/744330105a74dc004578f47ec27e1bf66b1dd5664ea444d18423e41343bd/mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d", size = 3150767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/0a/70de7c97a86cb85535077ab5cef1cbc4e2812fd2e9cc21d78eb561a6b80f/mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", size = 10940998 }, + { url = "https://files.pythonhosted.org/packages/c0/97/9ed6d4834d7549936ab88533b302184fb568a0940c4000d2aaee6dc07112/mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", size = 10108523 }, + { url = "https://files.pythonhosted.org/packages/48/41/1686f37d09c915dfc5b683e20cc99dabac199900b5ca6d22747b99ddcb50/mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", size = 12505553 }, + { url = "https://files.pythonhosted.org/packages/8d/2b/2dbcaa7e97b23f27ced77493256ee878f4a140ac750e198630ff1b9b60c6/mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", size = 12988634 }, + { url = "https://files.pythonhosted.org/packages/54/55/710d082e91a2ccaea21214229b11f9215a9d22446f949491b5457655e82b/mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", size = 9630747 }, + { url = "https://files.pythonhosted.org/packages/8a/74/b9e0e4f06e951e277058f878302faa154d282ca11274c59fe08353f52949/mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", size = 11079902 }, + { url = "https://files.pythonhosted.org/packages/9f/62/fcad290769db3eb0de265094cef5c94d6075c70bc1e42b67eee4ca192dcc/mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", size = 10072373 }, + { url = "https://files.pythonhosted.org/packages/cb/27/9ac78349c2952e4446288ec1174675ab9e0160ed18c2cb1154fa456c54e8/mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", size = 12589779 }, + { url = "https://files.pythonhosted.org/packages/7c/4a/58cebd122cf1cba95680ac51303fbeb508392413ca64e3e711aa7d4877aa/mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", size = 13044459 }, + { url = "https://files.pythonhosted.org/packages/5b/c7/672935e2a3f9bcc07b1b870395a653f665657bef3cdaa504ad99f56eadf0/mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", size = 9731919 }, + { url = "https://files.pythonhosted.org/packages/bb/b0/092be5094840a401940c95224f63bb2a8f09bce9251ac1df180ec523830c/mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", size = 11068611 }, + { url = "https://files.pythonhosted.org/packages/9a/86/f20f53b8f062876c39602243d7a59b5cabd6b24315d8de511d607fa4de6a/mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", size = 10068036 }, + { url = "https://files.pythonhosted.org/packages/84/c7/1dbd6575785522da1d4c1ac2c419505fcf23bee74811880cac447a4a77ab/mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", size = 12585671 }, + { url = "https://files.pythonhosted.org/packages/46/8a/f6ae18b446eb2bccce54c4bd94065bcfe417d6c67021dcc032bf1e720aff/mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", size = 13036083 }, + { url = "https://files.pythonhosted.org/packages/59/e6/fc65fde3dc7156fce8d49ba21c7b1f5d866ad50467bf196ca94a7f6d2c9e/mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", size = 9735467 }, + { url = "https://files.pythonhosted.org/packages/84/6b/1db9de4e0764778251fb2d64cb7455cf6db75dc99c9f72c8b7e74b6a8a17/mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", size = 2646060 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "orjson" +version = "3.10.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/3a/10320029954badc7eaa338a15ee279043436f396e965dafc169610e4933f/orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", size = 5444879 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/25/c869a1fbd481dcb02c70032fd6a7243de7582bc48c7cae03d6f0985a11c0/orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6", size = 266432 }, + { url = "https://files.pythonhosted.org/packages/6a/a4/2307155ee92457d28345308f7d8c0e712348404723025613adeffcb531d0/orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe", size = 151884 }, + { url = "https://files.pythonhosted.org/packages/aa/82/daf1b2596dd49fe44a1bd92367568faf6966dcb5d7f99fd437c3d0dc2de6/orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67", size = 167371 }, + { url = "https://files.pythonhosted.org/packages/63/a8/680578e4589be5fdcfe0186bdd7dc6fe4a39d30e293a9da833cbedd5a56e/orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b", size = 154368 }, + { url = "https://files.pythonhosted.org/packages/6e/ce/9cb394b5b01ef34579eeca6d704b21f97248f607067ce95a24ba9ea2698e/orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d", size = 165725 }, + { url = "https://files.pythonhosted.org/packages/49/24/55eeb05cfb36b9e950d05743e6f6fdb7d5f33ca951a27b06ea6d03371aed/orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5", size = 142522 }, + { url = "https://files.pythonhosted.org/packages/94/0c/3a6a289e56dcc9fe67dc6b6d33c91dc5491f9ec4a03745efd739d2acf0ff/orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a", size = 146934 }, + { url = "https://files.pythonhosted.org/packages/1d/5c/a08c0e90a91e2526029a4681ff8c6fc4495b8bab77d48801144e378c7da9/orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981", size = 142904 }, + { url = "https://files.pythonhosted.org/packages/2c/c9/710286a60b14e88288ca014d43befb08bb0a4a6a0f51b875f8c2f05e8205/orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55", size = 144459 }, + { url = "https://files.pythonhosted.org/packages/7d/68/ef7b920e0a09e02b1a30daca1b4864938463797995c2fabe457c1500220a/orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec", size = 136444 }, + { url = "https://files.pythonhosted.org/packages/78/f2/a712dbcef6d84ff53e13056e7dc69d9d4844bd1e35e51b7431679ddd154d/orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51", size = 266505 }, + { url = "https://files.pythonhosted.org/packages/94/54/53970831786d71f98fdc13c0f80451324c9b5c20fbf42f42ef6147607ee7/orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97", size = 151745 }, + { url = "https://files.pythonhosted.org/packages/35/38/482667da1ca7ef95d44d4d2328257a144fd2752383e688637c53ed474d2a/orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19", size = 167274 }, + { url = "https://files.pythonhosted.org/packages/23/2f/5bb0a03e819781d82dadb733fde8ebbe20d1777d1a33715d45ada4d82ce8/orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0", size = 154605 }, + { url = "https://files.pythonhosted.org/packages/49/e9/14cc34d45c7bd51665aff9b1bb6b83475a61c52edb0d753fffe1adc97764/orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433", size = 165874 }, + { url = "https://files.pythonhosted.org/packages/7b/61/c2781ecf90f99623e97c67a31e8553f38a1ecebaf3189485726ac8641576/orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5", size = 142813 }, + { url = "https://files.pythonhosted.org/packages/4d/4f/18c83f78b501b6608569b1610fcb5a25c9bb9ab6a7eb4b3a55131e0fba37/orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd", size = 146762 }, + { url = "https://files.pythonhosted.org/packages/ba/19/ea80d5b575abd3f76a790409c2b7b8a60f3fc9447965c27d09613b8bddf4/orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b", size = 143186 }, + { url = "https://files.pythonhosted.org/packages/2c/f5/d835fee01a0284d4b78effc24d16e7609daac2ff6b6851ca1bdd3b6194fc/orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d", size = 144489 }, + { url = "https://files.pythonhosted.org/packages/03/60/748e0e205060dec74328dfd835e47902eb5522ae011766da76bfff64e2f4/orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284", size = 136614 }, + { url = "https://files.pythonhosted.org/packages/13/92/400970baf46b987c058469e9e779fb7a40d54a5754914d3634cca417e054/orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", size = 266402 }, + { url = "https://files.pythonhosted.org/packages/3c/fa/f126fc2d817552bd1f67466205abdcbff64eab16f6844fe6df2853528675/orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", size = 140826 }, + { url = "https://files.pythonhosted.org/packages/ad/18/9b9664d7d4af5b4fe9fe6600b7654afc0684bba528260afdde10c4a530aa/orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", size = 142593 }, + { url = "https://files.pythonhosted.org/packages/20/f9/a30c68f12778d5e58e6b5cdd26f86ee2d0babce1a475073043f46fdd8402/orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", size = 146777 }, + { url = "https://files.pythonhosted.org/packages/f2/97/12047b0c0e9b391d589fb76eb40538f522edc664f650f8e352fdaaf77ff5/orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", size = 142961 }, + { url = "https://files.pythonhosted.org/packages/a4/97/d904e26c1cabf2dd6ab1b0909e9b790af28a7f0fcb9d8378d7320d4869eb/orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", size = 144486 }, + { url = "https://files.pythonhosted.org/packages/42/62/3760bd1e6e949321d99bab238d08db2b1266564d2f708af668f57109bb36/orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", size = 136361 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, +] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, + { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, + { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, + { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, + { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, + { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, + { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, + { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, + { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, + { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, + { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, + { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, + { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, + { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, + { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, + { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, + { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, + { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, + { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, + { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, + { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, + { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, + { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, + { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, + { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, + { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, + { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, + { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, + { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, + { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, + { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, + { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, + { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, + { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, + { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/32f05854cfd432e9286bb41a870e0d1a926b72df5f5cdb6dec962b2e369e/pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7", size = 840790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/32/95a164ddf533bd676cbbe878e36e89b4ade3efde8dd61d0148c90cbbe57e/pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", size = 263448 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "pytest-randomly" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396 }, +] + +[[package]] +name = "pytest-timeout" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "pywin32" +version = "308" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156 }, + { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559 }, + { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495 }, + { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, + { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, + { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, + { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, + { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, + { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, +] + +[[package]] +name = "rcslice" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180 }, +] + +[[package]] +name = "redis" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "13.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", size = 222889 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", size = 242157 }, +] + +[[package]] +name = "ruff" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, + { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, + { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, + { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, + { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, + { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, + { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, + { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, + { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, + { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, + { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, + { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, + { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, + { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, + { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, + { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.41.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, +] + +[[package]] +name = "testcontainers" +version = "4.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/72/c58d84f5704c6caadd9f803a3adad5ab54ac65328c02d13295f40860cf33/testcontainers-4.8.2.tar.gz", hash = "sha256:dd4a6a2ea09e3c3ecd39e180b6548105929d0bb78d665ce9919cb3f8c98f9853", size = 63590 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/77/5ac0dff2903a033d83d971fd85957356abdb66a327f3589df2b3d1a586b4/testcontainers-4.8.2-py3-none-any.whl", hash = "sha256:9e19af077cd96e1957c13ee466f1f32905bc6c5bc1bc98643eb18be1a989bfb0", size = 104326 }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "tomli" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, +] + +[[package]] +name = "tomli-w" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + +[[package]] +name = "trove-classifiers" +version = "2024.10.21.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/85/92c2667cf221b37648041ce9319427f92fa76cbec634aad844e67e284706/trove_classifiers-2024.10.21.16.tar.gz", hash = "sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3", size = 16153 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl", hash = "sha256:0fb11f1e995a757807a8ef1c03829fbd4998d817319abcef1f33165750f103be", size = 13546 }, +] + +[[package]] +name = "typer" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "userpath" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065 }, +] + +[[package]] +name = "uv" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/ad/66cc8e00c217e7fcf76598c880632b480aa38d4cad311596b78e99737498/uv-0.5.4.tar.gz", hash = "sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00", size = 2315678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/3e/6bf24d7bb0d11715ea783ecabcacdecdc8c51fca0144fcdad2090d65bae5/uv-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2", size = 13853445 }, + { url = "https://files.pythonhosted.org/packages/b8/be/c3acbe2944cd694a5d61a7a461468fa886512c84014545bb8f3244092eaa/uv-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58", size = 13969300 }, + { url = "https://files.pythonhosted.org/packages/1f/c5/06e3b93045179b92d75cf94e6e224baec3226070f1cbc0e11d4898300b54/uv-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4", size = 12932325 }, + { url = "https://files.pythonhosted.org/packages/b8/f9/06ab86e9f0c270c495077ef2b588458172ed84f9c337de725c8b08872354/uv-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39", size = 13183356 }, + { url = "https://files.pythonhosted.org/packages/c1/cb/bee01ef23e5020dc1f12d86ca8f82e95a723585db3ec64bfab4016e5616c/uv-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6", size = 13622310 }, + { url = "https://files.pythonhosted.org/packages/19/4b/128fd874151919c71af51f528db28964e6d8e509fff12210ec9ba99b13fb/uv-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263", size = 14207832 }, + { url = "https://files.pythonhosted.org/packages/b1/2b/0fed8a49440494f6806dcb67021ca8f14d46f45a665235fc153791e19574/uv-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83", size = 14878796 }, + { url = "https://files.pythonhosted.org/packages/c9/35/a6dc404d4d8884e26ad7bda004c101972fe7d81f86546a8628272812b897/uv-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25", size = 14687838 }, + { url = "https://files.pythonhosted.org/packages/74/9e/c2ebf66b90d48def06cda29626bb38068418ed135ca903beb293825ef66d/uv-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927", size = 18960541 }, + { url = "https://files.pythonhosted.org/packages/3d/67/28a8b4c23920ae1b1b0103ebae2fa176bd5677c4353b5e814a51bd183285/uv-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194", size = 14471756 }, + { url = "https://files.pythonhosted.org/packages/e9/1c/9698818f4c5493dfd5ab0899a90eee789cac214de2f171220bcdfaefc93a/uv-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01", size = 13389089 }, + { url = "https://files.pythonhosted.org/packages/0b/30/31a9985d84ffb63fb9212fa2b565497e0ceb581be055e5cc760afbe26b11/uv-0.5.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c", size = 13612748 }, + { url = "https://files.pythonhosted.org/packages/26/8d/bae613187ba88d74f0268246ce140f23d399bab96d2cbc055d6e4adafd09/uv-0.5.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876", size = 13946421 }, + { url = "https://files.pythonhosted.org/packages/0e/22/efd1eec81a566139bced68f4bd140c275edac3dac1bd6236cf8d756423db/uv-0.5.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119", size = 15752913 }, + { url = "https://files.pythonhosted.org/packages/49/b2/0cc4ae143b9605c25e75772aea22876b5875db79982ba62bb6f8d3099fab/uv-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc", size = 14599503 }, + { url = "https://files.pythonhosted.org/packages/51/9a/33d40a5068fd37c4f7b4fa82396e3ee90a691cd256f364ff398612c1d5d4/uv-0.5.4-py3-none-win32.whl", hash = "sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa", size = 13749570 }, + { url = "https://files.pythonhosted.org/packages/b1/c8/827e4da65cbdab2c1619767a68ab99a31de078e511b71ca9f24777df33f9/uv-0.5.4-py3-none-win_amd64.whl", hash = "sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb", size = 15573613 }, +] + +[[package]] +name = "uvicorn" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + +[[package]] +name = "virtualenv" +version = "20.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + +[[package]] +name = "watchfiles" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, + { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, + { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, + { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, + { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, + { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, + { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, + { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, + { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, + { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, + { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, + { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, + { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, + { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, + { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, + { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, + { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, + { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, + { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, + { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, + { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, + { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, + { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, + { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, + { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, + { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, + { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, + { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, + { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, + { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, + { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, + { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, + { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, + { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, + { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, + { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, + { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, +] + +[[package]] +name = "websockets" +version = "14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, + { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, + { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, + { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, + { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, + { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, + { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, + { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, + { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, + { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, + { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, + { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, + { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, + { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, + { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, + { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, + { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, + { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, + { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, + { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, + { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, + { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, + { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, + { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, + { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, + { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, + { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, + { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", size = 3676 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad", size = 3604 }, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313 }, + { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164 }, + { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890 }, + { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118 }, + { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746 }, + { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556 }, + { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712 }, + { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327 }, + { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523 }, + { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614 }, + { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316 }, + { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322 }, + { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055 }, + { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291 }, + { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374 }, + { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896 }, + { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738 }, + { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568 }, + { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653 }, + { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] + +[[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 }, +] diff --git a/.conflict-side-1/.github/workflows/ci.yml b/.conflict-side-1/.github/workflows/ci.yml new file mode 100644 index 0000000..5fb99bb --- /dev/null +++ b/.conflict-side-1/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras + + - name: Run Mypy + run: uv run mypy . + + test: + name: Test Python ${{ matrix.python }} + runs-on: "ubuntu-latest" + strategy: + fail-fast: true + matrix: + python: ["3.11", "3.12", "3.13"] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras --python ${{ matrix.python }} + + - name: Run unit tests + run: uv run pytest -x + + - name: Run integration tests + run: uv run pytest -x -m integration --cov-append + + - name: Rename coverage report + run: mv .coverage .coverage.py${{ matrix.python }} + + - name: Save coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-${{ matrix.python }} + path: .coverage.py${{ matrix.python }} + include-hidden-files: true + + coverage-report: + name: Coverage report + runs-on: ubuntu-latest + needs: [test] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Get coverage reports + uses: actions/download-artifact@v4 + with: + pattern: coverage-* + merge-multiple: true + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras + + - name: Combine coverage reports + run: | + uv run coverage combine .coverage.* + uv run coverage xml -o cov.xml + + - name: Upload coverage report to Codecov + uses: codecov/codecov-action@v4.0.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./cov.xml diff --git a/.conflict-side-1/.github/workflows/release.yml b/.conflict-side-1/.github/workflows/release.yml new file mode 100644 index 0000000..c8d4bab --- /dev/null +++ b/.conflict-side-1/.github/workflows/release.yml @@ -0,0 +1,110 @@ +name: Release + +on: + release: + types: + - published + +jobs: + bump-version: + name: Bump version + runs-on: ubuntu-latest + steps: + + - name: Generate GitHub App Token + uses: actions/create-github-app-token@v1 + id: app-token + with: + app-id: ${{ secrets.GRELINFO_ID }} + private-key: ${{ secrets.GRELINFO_KEY }} + + - name: Get GitHub App User ID + id: user-id + run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + - name: Configure Git App Credentials + run: | + git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' + git config --global user.email '${{ steps.user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}@users.noreply.github.com>' + + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Get release version + id: release-version + run: echo "release-version=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" + + - name: Get current version + id: current-version + run: echo "current-version=$(uv run hatch version)" >> "$GITHUB_OUTPUT" + + - name: Bump version if necessary + if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} + run: | + uv run hatch version $RELEASE_VERSION + uv lock + + - name: Commit and push changes + if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} + run: | + git add . + git commit -m "🚀 Release $RELEASE_VERSION" + git tag -f $RELEASE_VERSION + git push origin $RELEASE_VERSION --force + git push origin HEAD:main + + publish-docs: + runs-on: ubuntu-latest + needs: [bump-version] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Configure Git Credentials + run: | + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --group docs + + - name: Deploy docs on GitHub Pages + run: uv run mkdocs gh-deploy --force + + publish-pypi: + needs: [bump-version] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.ref_name }} + + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + enable-cache: true + + - name: Build + run: uv build + + - name: Publish + run: uv publish -t ${{ secrets.PYPI_TOKEN }} diff --git a/.conflict-side-1/.gitignore b/.conflict-side-1/.gitignore new file mode 100644 index 0000000..0d118ab --- /dev/null +++ b/.conflict-side-1/.gitignore @@ -0,0 +1,17 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Coverage +cov.xml +.coverage + +# Mkdocs +site/ diff --git a/.conflict-side-1/.pre-commit-config.yaml b/.conflict-side-1/.pre-commit-config.yaml new file mode 100644 index 0000000..5e5a141 --- /dev/null +++ b/.conflict-side-1/.pre-commit-config.yaml @@ -0,0 +1,63 @@ +default_language_version: + python: python3.11 + +repos: + +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: end-of-file-fixer + - id: check-toml + - id: check-yaml + - id: check-added-large-files + - id: trailing-whitespace + +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.0 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + +- repo: local + hooks: + + - id: readme-to-docs + name: readme-to-docs + description: "Copy README.md to docs/index.md" + entry: cp README.md docs/index.md + language: system + pass_filenames: false + + # --- Local development hooks --- + - id: uv-lock + name: uv-lock + description: "Lock dependencies with 'uv lock'" + entry: uv lock + language: system + pass_filenames: false + + - id: mypy + name: mypy + description: "Run 'mypy' for static type checking" + entry: uv run mypy + language: system + types: [python] + require_serial: true + + - id: pytest + name: pytest + description: "Run 'pytest' for unit testing" + entry: uv run pytest --cov-fail-under=90 + language: system + pass_filenames: false + +ci: + autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks + autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate + skip: [uv-lock, mypy, pytest] diff --git a/.conflict-side-1/.vscode/settings.json b/.conflict-side-1/.vscode/settings.json new file mode 100644 index 0000000..806ffc4 --- /dev/null +++ b/.conflict-side-1/.vscode/settings.json @@ -0,0 +1,58 @@ +{ + // Editor settings + "editor.rulers": [80, 100], + "files.trimTrailingWhitespace": true, + "terminal.integrated.scrollback": 10000, + + // Files exclude settings + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/__pycache__": true, + "**/.venv": true, + "**/.mypy_cache": true, + "**/.pytest_cache": true, + "**/.ruff_cache": true, + ".coverage": true + }, + + // Python settings + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.activateEnvironment": true, + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestArgs": ["--no-cov", "--color=yes"], + "python.analysis.inlayHints.pytestParameters": true, + + // Python editor settings + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + + // Mypy settings + "mypy-type-checker.importStrategy": "fromEnvironment", + + // YAML settings + "yaml.schemas": { + "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" + }, + "yaml.customTags": [ + "!ENV scalar", + "!ENV sequence", + "!relative scalar", + "tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg", + "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", + "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", + "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" + ], + + // Ruff settings + "ruff.configurationPreference": "filesystemFirst" +} diff --git a/.conflict-side-1/LICENSE b/.conflict-side-1/LICENSE new file mode 100644 index 0000000..18dafa2 --- /dev/null +++ b/.conflict-side-1/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Loïc Gremaud + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.conflict-side-1/README.md b/.conflict-side-1/README.md new file mode 100644 index 0000000..9f3e0ff --- /dev/null +++ b/.conflict-side-1/README.md @@ -0,0 +1,158 @@ +# Grelmicro + +Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. + +It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. + +[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) +[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) + +______________________________________________________________________ + +**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) + +**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) + +______________________________________________________________________ + +## Overview + +Grelmicro provides essential features for building robust distributed systems, including: + +- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. +- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. +- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. +- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. + +These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. + +### Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. + +### Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. + +The available primitives are: + +- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **Lock**: A distributed lock that can be used to synchronize access to shared resources. + +### Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Installation + +```bash +pip install grelmicro +``` + +## Examples + +### FastAPI Integration + +- Create a file `main.py` with: + +```python +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") +``` + +## Dependencies + +Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. + +### `standard` Dependencies + +When you install Grelmicro with `pip install grelmicro[standard]` it comes with: + +- `loguru`: A Python logging library. +- `orjson`: A fast, correct JSON library for Python. + +### `redis` Dependencies + +When you install Grelmicro with `pip install grelmicro[redis]` it comes with: + +- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). + +### `postgres` Dependencies + +When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: + +- `asyncpg`: The Python `asyncio` interface for PostgreSQL. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/.conflict-side-1/docs/index.md b/.conflict-side-1/docs/index.md new file mode 100644 index 0000000..9f3e0ff --- /dev/null +++ b/.conflict-side-1/docs/index.md @@ -0,0 +1,158 @@ +# Grelmicro + +Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. + +It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. + +[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) +[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) + +______________________________________________________________________ + +**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) + +**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) + +______________________________________________________________________ + +## Overview + +Grelmicro provides essential features for building robust distributed systems, including: + +- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. +- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. +- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. +- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. + +These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. + +### Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. + +### Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. + +The available primitives are: + +- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **Lock**: A distributed lock that can be used to synchronize access to shared resources. + +### Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Installation + +```bash +pip install grelmicro +``` + +## Examples + +### FastAPI Integration + +- Create a file `main.py` with: + +```python +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") +``` + +## Dependencies + +Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. + +### `standard` Dependencies + +When you install Grelmicro with `pip install grelmicro[standard]` it comes with: + +- `loguru`: A Python logging library. +- `orjson`: A fast, correct JSON library for Python. + +### `redis` Dependencies + +When you install Grelmicro with `pip install grelmicro[redis]` it comes with: + +- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). + +### `postgres` Dependencies + +When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: + +- `asyncpg`: The Python `asyncio` interface for PostgreSQL. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/.conflict-side-1/docs/logging.md b/.conflict-side-1/docs/logging.md new file mode 100644 index 0000000..4575b03 --- /dev/null +++ b/.conflict-side-1/docs/logging.md @@ -0,0 +1,73 @@ +# Logging + +The `logging` package provides a simple and easy-to-configure logging system. + +The logging feature adheres to the 12-factor app methodology, directing logs to stdout. It supports JSON formatting and allows log level configuration via environment variables. + +## Dependencies + +For the moment the `logging` package is only working with the `loguru` Python logging library. +When `orjson` is installed, it will be used as the default JSON serializer for faster performance, otherwise, the standard `json` library will be used. + +[**Loguru**](https://loguru.readthedocs.io/en/stable/overview.html) is used as the logging library. + +For using `logging` package, please install the required dependencies: + +=== "Standard" + ```bash + pip install grelmicro[standard] + ``` + +=== "only loguru (minimum)" + ```bash + pip install loguru + ``` + +=== "loguru and orjson (manual)" + ```bash + pip install loguru orjson + ``` + + +## Configure Logging + +Just call the `configure_logging` function to set up the logging system. + +```python +{!> ../examples/logging/configure_logging.py!} +``` + +### Settings + +You can change the default settings using the following environment variables: + +- `LOG_LEVEL`: Set the desired log level (default: `INFO`). +- `LOG_FORMAT`: Choose the log format. Options are `TEXT` and `JSON`, or you can provide a custom [loguru](https://loguru.readthedocs.io/en/stable/overview.html) template (default: `TEXT`). + + +## Examples + +### Basic Usage + +Here is a quick example of how to use the logging system: + +```python +{!> ../examples/logging/basic.py!} +``` + +The console output, `stdout` will be: + +```json +{!> ../examples/logging/basic.log!} +``` + +### FastAPI Integration + +You can use the logging system with FastAPI as well: + +```python +{!> ../examples/logging/fastapi.py!} +``` + +!!! warning + It is crucial to call `configure_logging` during the lifespan of the FastAPI application. Failing to do so may result in the FastAPI CLI resetting the logging configuration. diff --git a/.conflict-side-1/docs/sync.md b/.conflict-side-1/docs/sync.md new file mode 100644 index 0000000..4c3b881 --- /dev/null +++ b/.conflict-side-1/docs/sync.md @@ -0,0 +1,81 @@ +# Synchronization Primitives + +The `sync` package provides synchronization primitives for distributed systems. + +The primitives are technology agnostic, supporting multiple backends (see more in the Backends section). + +The available primitives are: + +- **[Leader Election](#leader-election)**: A single worker is elected as the leader for performing tasks only once in a cluster. +- **[Lock](#lock)**: A distributed lock that can be used to synchronize access to shared resources. + +The synchronization primitives can be used in combination with the `TaskManager` and `TaskRouter` to control task execution in a distributed system (see more in [Task Scheduler](task.md)). + +## Backend + +You must load a synchronization backend before using synchronization primitives. + +!!! note + Although Grelmicro use AnyIO for concurrency, the backends generally depend on `asyncio`, therefore Trio is not supported. + +You can initialize a backend like this: + +=== "Redis" + ```python + {!> ../examples/sync/redis.py!} + ``` + +=== "Postgres" + ```python + {!> ../examples/sync/postgres.py!} + ``` + +=== "Memory (For Testing Only)" + ```python + {!> ../examples/sync/memory.py!} + ``` + +!!! warning + Please make sure to use a proper way to store connection url, such as environment variables (not like the example above). + +!!! tip + Feel free to create your own backend and contribute it. In the `sync.abc` module, you can find the protocol for creating new backends. + + + +## Leader Election + +Leader election ensures that only one worker in the cluster is designated as the leader at any given time using a distributed lock. + +The leader election service is responsible for acquiring and renewing the distributed lock. It runs as an AnyIO Task that can be easily started with the [Task Manager](./task.md#task-manager). This service operates in the background, automatically renewing the lock to prevent other workers from acquiring it. The lock is released automatically when the task is cancelled or during shutdown. + +=== "Task Manager (Recommended)" + ```python + {!> ../examples/sync/leaderelection_task.py!} + ``` + +=== "AnyIO Task Group (Advanced)" + ```python + {!> ../examples/sync/leaderelection_anyio.py!} + ``` + +## Lock + +The lock is a distributed lock that can be used to synchronize access to shared resources. + +The lock supports the following features: + +- **Async**: The lock must be acquired and released asynchronously. +- **Distributed**: The lock must be distributed across multiple workers. +- **Reentrant**: The lock must allow the same token to acquire it multiple times to extend the lease. +- **Expiring**: The lock must have a timeout to auto-release after an interval to prevent deadlocks. +- **Non-blocking**: Lock operations must not block the async event loop. +- **Vendor-agnostic**: Must support multiple backends (Redis, Postgres, ConfigMap, etc.). + + +```python +{!> ../examples/sync/lock.py!} +``` + +!!! warning + The lock is designed for use within an async event loop and is not thread-safe or process-safe. diff --git a/.conflict-side-1/docs/task.md b/.conflict-side-1/docs/task.md new file mode 100644 index 0000000..b6f0e00 --- /dev/null +++ b/.conflict-side-1/docs/task.md @@ -0,0 +1,85 @@ +# Task Scheduler + +The `task` package provides a simple task scheduler that can be used to run tasks periodically. + +> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. + +The key features are: + +- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. +- **Interval Task**: Allows tasks to run at specified intervals. +- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). +- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. +- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. + +## Task Manager + +The `TaskManager` class is the main entry point to manage scheduled tasks. You need to start the task manager to run the scheduled tasks using the application lifespan. + +=== "FastAPI" + + ```python + {!> ../examples/task/fastapi.py!} + ``` + +=== "FastStream" + + ```python + + {!> ../examples/task/faststream.py!} + ``` + +## Interval Task + +To create an `IntervalTask`, use the `interval` decorator method of the `TaskManager` instance. This decorator allows tasks to run at specified intervals. + +> **Note**: The interval specifies the waiting time between task executions. Ensure that the task execution duration is considered to meet deadlines effectively. + +=== "TaskManager" + + ```python + {!> ../examples/task/interval_manager.py!} + ``` + +=== "TaskRouter" + + ```python + {!> ../examples/task/interval_router.py!} + ``` + + +## Synchronization + +The Task can be synchronized using a [Synchoronization Primitive](sync.md) to control concurrency and manage simultaneous task execution. + +=== "Lock" + + ```python + {!> ../examples/task/lock.py!} + ``` + + +=== "Leader Election" + + + ```python + {!> ../examples/task/leaderelection.py!} + ``` + +## Task Router + +For bigger applications, you can use the `TaskRouter` class to manage tasks in different modules. + + +```python +{!> ../examples/task/router.py [ln:1-10]!} +``` + +Then you can include the `TaskRouter` into the `TaskManager` or other routers using the `include_router` method. + +```python +{!> ../examples/task/router.py [ln:12-]!} +``` + +!!! tip + The `TaskRouter` follows the same philosophy as the `APIRouter` in FastAPI or the **Router** in FastStream. diff --git a/.conflict-side-1/examples/__init__.py b/.conflict-side-1/examples/__init__.py new file mode 100644 index 0000000..73b7d32 --- /dev/null +++ b/.conflict-side-1/examples/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Examples.""" diff --git a/.conflict-side-1/examples/logging/__init__.py b/.conflict-side-1/examples/logging/__init__.py new file mode 100644 index 0000000..bf04afe --- /dev/null +++ b/.conflict-side-1/examples/logging/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Logging Examples.""" diff --git a/.conflict-side-1/examples/logging/basic.log b/.conflict-side-1/examples/logging/basic.log new file mode 100644 index 0000000..33c8e37 --- /dev/null +++ b/.conflict-side-1/examples/logging/basic.log @@ -0,0 +1,4 @@ +{"time":"2024-11-25T15:56:36.066922+01:00","level":"INFO","thread":"MainThread","logger":"__main__::7","msg":"This is an info message"} +{"time":"2024-11-25T15:56:36.067063+01:00","level":"WARNING","thread":"MainThread","logger":"__main__::8","msg":"This is a warning message with context","ctx":{"user":"Alice"}} +{"time":"2024-11-25T15:56:36.067105+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::9","msg":"This is an error message with context","ctx":{"user":"Bob"}} +{"time":"2024-11-25T15:56:36.067134+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::14","msg":"This is an exception message with context","ctx":{"user":"Charlie","exception":"ValueError: This is an exception"}} diff --git a/.conflict-side-1/examples/logging/basic.py b/.conflict-side-1/examples/logging/basic.py new file mode 100644 index 0000000..889f160 --- /dev/null +++ b/.conflict-side-1/examples/logging/basic.py @@ -0,0 +1,17 @@ +from loguru import logger + +from grelmicro.logging import configure_logging + +configure_logging() + +logger.debug("This is a debug message") +logger.info("This is an info message") +logger.warning("This is a warning message with context", user="Alice") +logger.error("This is an error message with context", user="Bob") + +try: + raise ValueError("This is an exception message") +except ValueError: + logger.exception( + "This is an exception message with context", user="Charlie" + ) diff --git a/.conflict-side-1/examples/logging/configure_logging.py b/.conflict-side-1/examples/logging/configure_logging.py new file mode 100644 index 0000000..0ffacd8 --- /dev/null +++ b/.conflict-side-1/examples/logging/configure_logging.py @@ -0,0 +1,3 @@ +from grelmicro.logging import configure_logging + +configure_logging() diff --git a/.conflict-side-1/examples/logging/fastapi.py b/.conflict-side-1/examples/logging/fastapi.py new file mode 100644 index 0000000..7f318c5 --- /dev/null +++ b/.conflict-side-1/examples/logging/fastapi.py @@ -0,0 +1,22 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from loguru import logger + +from grelmicro.logging import configure_logging + + +@asynccontextmanager +def lifespan_startup(): + # Ensure logging is configured during startup + configure_logging() + yield + + +app = FastAPI() + + +@app.get("/") +def root(): + logger.info("This is an info message") + return {"Hello": "World"} diff --git a/.conflict-side-1/examples/simple_fastapi_app.py b/.conflict-side-1/examples/simple_fastapi_app.py new file mode 100644 index 0000000..ff52251 --- /dev/null +++ b/.conflict-side-1/examples/simple_fastapi_app.py @@ -0,0 +1,54 @@ +from contextlib import asynccontextmanager + +import typer +from fastapi import FastAPI + +from grelmicro.logging.loguru import configure_logging +from grelmicro.sync import LeaderElection, Lock +from grelmicro.sync.redis import RedisSyncBackend +from grelmicro.task import TaskManager + + +# === FastAPI === +@asynccontextmanager +async def lifespan(app): + configure_logging() + # Start the lock backend and task manager + async with sync_backend, task: + yield + + +app = FastAPI(lifespan=lifespan) + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +# === Grelmicro === +task = TaskManager() +sync_backend = RedisSyncBackend("redis://localhost:6379/0") + +# --- Ensure that only one say hello world at the same time --- +lock = Lock("say_hello_world") + + +@task.interval(seconds=1, sync=lock) +def say_hello_world_every_second(): + typer.echo("Hello World") + + +@task.interval(seconds=1, sync=lock) +def say_as_well_hello_world_every_second(): + typer.echo("Hello World") + + +# --- Ensure that only one worker is the leader --- +leader_election = LeaderElection("leader-election") +task.add_task(leader_election) + + +@task.interval(seconds=10, sync=leader_election) +def say_hello_leader_every_ten_seconds(): + typer.echo("Hello Leader") diff --git a/.conflict-side-1/examples/single_file_app.py b/.conflict-side-1/examples/single_file_app.py new file mode 100644 index 0000000..4f4bb87 --- /dev/null +++ b/.conflict-side-1/examples/single_file_app.py @@ -0,0 +1,114 @@ +import time +from contextlib import asynccontextmanager +from typing import Annotated + +import anyio +import typer +from fast_depends import Depends +from fastapi import FastAPI + +from grelmicro.sync.leaderelection import LeaderElection +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.task import TaskManager + +backend = MemorySyncBackend() +task = TaskManager() + + +@asynccontextmanager +async def lifespan(app): + async with backend, task: + typer.echo("App started") + yield + typer.echo("App stopped") + + +app = FastAPI(lifespan=lifespan) + +leased_lock_10sec = Lock( + name="leased_lock_10sec", + lease_duration=10, + backend=backend, +) +leased_lock_5sec = Lock( + name="leased_lock_5sec", + lease_duration=5, + backend=backend, +) + +leader_election = LeaderElection(name="simple-leader", backend=backend) + +task.add_task(leader_election) + + +@task.interval(seconds=1) +def sync_func_with_no_param(): + typer.echo("sync_with_no_param") + + +@task.interval(seconds=2) +async def async_func_with_no_param(): + typer.echo("async_with_no_param") + + +def sync_dependency(): + return "sync_dependency" + + +@task.interval(seconds=3) +def sync_func_with_sync_dependency( + sync_dependency: Annotated[str, Depends(sync_dependency)], +): + typer.echo(sync_dependency) + + +async def async_dependency(): + yield "async_with_async_dependency" + + +@task.interval(seconds=4) +async def async_func_with_async_dependency( + async_dependency: Annotated[str, Depends(async_dependency)], +): + typer.echo(async_dependency) + + +@task.interval(seconds=15, sync=leased_lock_10sec) +def sync_func_with_leased_lock_10sec(): + typer.echo("sync_func_with_leased_lock_10sec") + time.sleep(9) + + +@task.interval(seconds=15, sync=leased_lock_10sec) +async def async_func_with_leased_lock_10sec(): + typer.echo("async_func_with_leased_lock_10sec") + await anyio.sleep(9) + + +@task.interval(seconds=15, sync=leased_lock_5sec) +def sync_func_with_sync_dependency_and_leased_lock_5sec( + sync_dependency: Annotated[str, Depends(sync_dependency)], +): + typer.echo(sync_dependency) + time.sleep(4) + + +@task.interval(seconds=15, sync=leased_lock_5sec) +async def async_func_with_async_dependency_and_leased_lock_5sec( + async_dependency: Annotated[str, Depends(async_dependency)], +): + typer.echo(async_dependency) + await anyio.sleep(4) + + +@task.interval(seconds=15, sync=leader_election) +def sync_func_with_leader_election(): + typer.echo("sync_func_with_leader_election") + time.sleep(30) + + +@task.interval(seconds=15, sync=leader_election) +async def async_func_with_leader_election(): + typer.echo("async_func_with_leader_election") + await anyio.sleep(30) diff --git a/.conflict-side-1/examples/sync/__init__.py b/.conflict-side-1/examples/sync/__init__.py new file mode 100644 index 0000000..acd409a --- /dev/null +++ b/.conflict-side-1/examples/sync/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Synchronization Primitives Examples.""" diff --git a/.conflict-side-1/examples/sync/leaderelection_anyio.py b/.conflict-side-1/examples/sync/leaderelection_anyio.py new file mode 100644 index 0000000..784f188 --- /dev/null +++ b/.conflict-side-1/examples/sync/leaderelection_anyio.py @@ -0,0 +1,11 @@ +from anyio import create_task_group, sleep_forever + +from grelmicro.sync.leaderelection import LeaderElection + +leader = LeaderElection("cluster_group") + + +async def main(): + async with create_task_group() as tg: + await tg.start(leader) + await sleep_forever() diff --git a/.conflict-side-1/examples/sync/leaderelection_task.py b/.conflict-side-1/examples/sync/leaderelection_task.py new file mode 100644 index 0000000..58fa926 --- /dev/null +++ b/.conflict-side-1/examples/sync/leaderelection_task.py @@ -0,0 +1,6 @@ +from grelmicro.sync import LeaderElection +from grelmicro.task import TaskManager + +leader = LeaderElection("cluster_group") +task = TaskManager() +task.add_task(leader) diff --git a/.conflict-side-1/examples/sync/lock.py b/.conflict-side-1/examples/sync/lock.py new file mode 100644 index 0000000..7f38fe6 --- /dev/null +++ b/.conflict-side-1/examples/sync/lock.py @@ -0,0 +1,8 @@ +from grelmicro.sync import Lock + +lock = Lock("resource_name") + + +async def main(): + async with lock: + print("Protected resource accessed") diff --git a/.conflict-side-1/examples/sync/memory.py b/.conflict-side-1/examples/sync/memory.py new file mode 100644 index 0000000..7eefea9 --- /dev/null +++ b/.conflict-side-1/examples/sync/memory.py @@ -0,0 +1,3 @@ +from grelmicro.sync.memory import MemorySyncBackend + +backend = MemorySyncBackend() diff --git a/.conflict-side-1/examples/sync/postgres.py b/.conflict-side-1/examples/sync/postgres.py new file mode 100644 index 0000000..ea8b8c3 --- /dev/null +++ b/.conflict-side-1/examples/sync/postgres.py @@ -0,0 +1,3 @@ +from grelmicro.sync.postgres import PostgresSyncBackend + +backend = PostgresSyncBackend("postgresql://user:password@localhost:5432/db") diff --git a/.conflict-side-1/examples/sync/redis.py b/.conflict-side-1/examples/sync/redis.py new file mode 100644 index 0000000..0625f5d --- /dev/null +++ b/.conflict-side-1/examples/sync/redis.py @@ -0,0 +1,3 @@ +from grelmicro.sync.redis import RedisSyncBackend + +backend = RedisSyncBackend("redis://localhost:6379/0") diff --git a/.conflict-side-1/examples/task/__init__.py b/.conflict-side-1/examples/task/__init__.py new file mode 100644 index 0000000..20f7752 --- /dev/null +++ b/.conflict-side-1/examples/task/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Task Scheduler Examples.""" diff --git a/.conflict-side-1/examples/task/fastapi.py b/.conflict-side-1/examples/task/fastapi.py new file mode 100644 index 0000000..16aaa8e --- /dev/null +++ b/.conflict-side-1/examples/task/fastapi.py @@ -0,0 +1,16 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from grelmicro.task import TaskManager + +task = TaskManager() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + async with task: + yield + + +app = FastAPI(lifespan=lifespan) diff --git a/.conflict-side-1/examples/task/faststream.py b/.conflict-side-1/examples/task/faststream.py new file mode 100644 index 0000000..688c8d9 --- /dev/null +++ b/.conflict-side-1/examples/task/faststream.py @@ -0,0 +1,18 @@ +from contextlib import asynccontextmanager + +from faststream import ContextRepo, FastStream +from faststream.redis import RedisBroker + +from grelmicro.task import TaskManager + +task = TaskManager() + + +@asynccontextmanager +async def lifespan(context: ContextRepo): + async with task: + yield + + +broker = RedisBroker() +app = FastStream(broker, lifespan=lifespan) diff --git a/.conflict-side-1/examples/task/interval_manager.py b/.conflict-side-1/examples/task/interval_manager.py new file mode 100644 index 0000000..91beb2e --- /dev/null +++ b/.conflict-side-1/examples/task/interval_manager.py @@ -0,0 +1,8 @@ +from grelmicro.task import TaskManager + +task = TaskManager() + + +@task.interval(seconds=5) +async def my_task(): + print("Hello, World!") diff --git a/.conflict-side-1/examples/task/interval_router.py b/.conflict-side-1/examples/task/interval_router.py new file mode 100644 index 0000000..f114ad7 --- /dev/null +++ b/.conflict-side-1/examples/task/interval_router.py @@ -0,0 +1,8 @@ +from grelmicro.task import TaskRouter + +task = TaskRouter() + + +@task.interval(seconds=5) +async def my_task(): + print("Hello, World!") diff --git a/.conflict-side-1/examples/task/leaderelection.py b/.conflict-side-1/examples/task/leaderelection.py new file mode 100644 index 0000000..ad12773 --- /dev/null +++ b/.conflict-side-1/examples/task/leaderelection.py @@ -0,0 +1,12 @@ +from grelmicro.sync import LeaderElection +from grelmicro.task import TaskManager + +leader = LeaderElection("my_task") +task = TaskManager() +task.add_task(leader) + + +@task.interval(seconds=5, sync=leader) +async def my_task(): + async with leader: + print("Hello, World!") diff --git a/.conflict-side-1/examples/task/lock.py b/.conflict-side-1/examples/task/lock.py new file mode 100644 index 0000000..cdbf795 --- /dev/null +++ b/.conflict-side-1/examples/task/lock.py @@ -0,0 +1,11 @@ +from grelmicro.sync import Lock +from grelmicro.task import TaskManager + +lock = Lock("my_task") +task = TaskManager() + + +@task.interval(seconds=5, sync=lock) +async def my_task(): + async with lock: + print("Hello, World!") diff --git a/.conflict-side-1/examples/task/router.py b/.conflict-side-1/examples/task/router.py new file mode 100644 index 0000000..2b166aa --- /dev/null +++ b/.conflict-side-1/examples/task/router.py @@ -0,0 +1,15 @@ +from grelmicro.task import TaskRouter + + +router = TaskRouter() + + +@router.interval(seconds=5) +async def my_task(): + print("Hello, World!") + + +from grelmicro.task.manager import TaskManager + +task = TaskManager() +task.include_router(router) diff --git a/.conflict-side-1/grelmicro/__init__.py b/.conflict-side-1/grelmicro/__init__.py new file mode 100644 index 0000000..7cc6d82 --- /dev/null +++ b/.conflict-side-1/grelmicro/__init__.py @@ -0,0 +1,3 @@ +"""Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python.""" # noqa: E501 + +__version__ = "0.2.2" diff --git a/.conflict-side-1/grelmicro/errors.py b/.conflict-side-1/grelmicro/errors.py new file mode 100644 index 0000000..141f82e --- /dev/null +++ b/.conflict-side-1/grelmicro/errors.py @@ -0,0 +1,52 @@ +"""Grelmicro Errors.""" + +from typing import assert_never + +from pydantic import ValidationError + + +class GrelmicroError(Exception): + """Base Grelmicro error.""" + + +class OutOfContextError(GrelmicroError, RuntimeError): + """Outside Context Error. + + Raised when a method is called outside of the context manager. + """ + + def __init__(self, cls: object, method_name: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not call {cls.__class__.__name__}.{method_name} outside of the context manager" + ) + + +class DependencyNotFoundError(GrelmicroError, ImportError): + """Dependency Not Found Error.""" + + def __init__(self, *, module: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not import module {module}, try running 'pip install {module}'" + ) + + +class SettingsValidationError(GrelmicroError, ValueError): + """Settings Validation Error.""" + + def __init__(self, error: ValidationError | str) -> None: + """Initialize the error.""" + if isinstance(error, str): + details = error + elif isinstance(error, ValidationError): + details = "\n".join( + f"- {data['loc'][0]}: {data['msg']} [input={data['input']}]" + for data in error.errors() + ) + else: + assert_never(error) + + super().__init__( + f"Could not validate environment variables settings:\n{details}" + ) diff --git a/.conflict-side-1/grelmicro/logging/__init__.py b/.conflict-side-1/grelmicro/logging/__init__.py new file mode 100644 index 0000000..60d3d45 --- /dev/null +++ b/.conflict-side-1/grelmicro/logging/__init__.py @@ -0,0 +1,5 @@ +"""Grelmicro Logging.""" + +from grelmicro.logging.loguru import configure_logging + +__all__ = ["configure_logging"] diff --git a/.conflict-side-1/grelmicro/logging/config.py b/.conflict-side-1/grelmicro/logging/config.py new file mode 100644 index 0000000..a6301c1 --- /dev/null +++ b/.conflict-side-1/grelmicro/logging/config.py @@ -0,0 +1,43 @@ +"""Logging Configuration.""" + +from enum import StrEnum +from typing import Self + +from pydantic import Field +from pydantic_settings import BaseSettings + + +class _CaseInsensitiveEnum(StrEnum): + @classmethod + def _missing_(cls, value: object) -> Self | None: + value = str(value).lower() + for member in cls: + if member.lower() == value: + return member + return None + + +class LoggingLevelType(_CaseInsensitiveEnum): + """Logging Level Enum.""" + + DEBUG = "DEBUG" + INFO = "INFO" + WARNING = "WARNING" + ERROR = "ERROR" + CRITICAL = "CRITICAL" + + +class LoggingFormatType(_CaseInsensitiveEnum): + """Logging Format Enum.""" + + JSON = "JSON" + TEXT = "TEXT" + + +class LoggingSettings(BaseSettings): + """Logging Settings.""" + + LOG_LEVEL: LoggingLevelType = LoggingLevelType.INFO + LOG_FORMAT: LoggingFormatType | str = Field( + LoggingFormatType.JSON, union_mode="left_to_right" + ) diff --git a/.conflict-side-1/grelmicro/logging/errors.py b/.conflict-side-1/grelmicro/logging/errors.py new file mode 100644 index 0000000..097006f --- /dev/null +++ b/.conflict-side-1/grelmicro/logging/errors.py @@ -0,0 +1,7 @@ +"""Grelmicro Logging Errors.""" + +from grelmicro.errors import SettingsValidationError + + +class LoggingSettingsValidationError(SettingsValidationError): + """Logging Settings Validation Error.""" diff --git a/.conflict-side-1/grelmicro/logging/loguru.py b/.conflict-side-1/grelmicro/logging/loguru.py new file mode 100644 index 0000000..a94202c --- /dev/null +++ b/.conflict-side-1/grelmicro/logging/loguru.py @@ -0,0 +1,121 @@ +"""Loguru Logging.""" + +import json +import sys +from collections.abc import Mapping +from typing import TYPE_CHECKING, Any, NotRequired + +from pydantic import ValidationError +from typing_extensions import TypedDict + +from grelmicro.errors import DependencyNotFoundError +from grelmicro.logging.config import LoggingFormatType, LoggingSettings +from grelmicro.logging.errors import LoggingSettingsValidationError + +if TYPE_CHECKING: + from loguru import FormatFunction, Record + +try: + import loguru +except ImportError: # pragma: no cover + loguru = None # type: ignore[assignment] + +try: + import orjson + + def _json_dumps(obj: Mapping[str, Any]) -> str: + return orjson.dumps(obj).decode("utf-8") +except ImportError: # pragma: no cover + import json + + _json_dumps = json.dumps + + +JSON_FORMAT = "{extra[serialized]}" +TEXT_FORMAT = ( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " + "{name}:{function}:{line} - {message}" +) + + +class JSONRecordDict(TypedDict): + """JSON log record representation. + + The time use a ISO 8601 string. + """ + + time: str + level: str + msg: str + logger: str | None + thread: str + ctx: NotRequired[dict[Any, Any]] + + +def json_patcher(record: "Record") -> None: + """Patch the serialized log record with `JSONRecordDict` representation.""" + json_record = JSONRecordDict( + time=record["time"].isoformat(), + level=record["level"].name, + thread=record["thread"].name, + logger=f'{record["name"]}:{record["function"]}:{record["line"]}', + msg=record["message"], + ) + + ctx = {k: v for k, v in record["extra"].items() if k != "serialized"} + exception = record["exception"] + + if exception and exception.type: + ctx["exception"] = f"{exception.type.__name__}: {exception.value!s}" + + if ctx: + json_record["ctx"] = ctx + + record["extra"]["serialized"] = _json_dumps(json_record) + + +def json_formatter(record: "Record") -> str: + """Format log record with `JSONRecordDict` representation. + + This function does not return the formatted record directly but provides the format to use when + writing to the sink. + """ + json_patcher(record) + return JSON_FORMAT + "\n" + + +def configure_logging() -> None: + """Configure logging with loguru. + + Simple twelve-factor app logging configuration that logs to stdout. + + The following environment variables are used: + - LOG_LEVEL: The log level to use (default: INFO). + - LOG_FORMAT: JSON | TEXT or any loguru template to format logged message (default: JSON). + + Raises: + MissingDependencyError: If the loguru module is not installed. + LoggingSettingsError: If the LOG_FORMAT or LOG_LEVEL environment variable is invalid + """ + if not loguru: + raise DependencyNotFoundError(module="loguru") + + try: + settings = LoggingSettings() + except ValidationError as error: + raise LoggingSettingsValidationError(error) from None + + logger = loguru.logger + log_format: str | FormatFunction = settings.LOG_FORMAT + + if log_format is LoggingFormatType.JSON: + log_format = json_formatter + elif log_format is LoggingFormatType.TEXT: + log_format = TEXT_FORMAT + + logger.remove() + logger.add( + sys.stdout, + level=settings.LOG_LEVEL, + format=log_format, + ) diff --git a/.conflict-side-1/grelmicro/py.typed b/.conflict-side-1/grelmicro/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/.conflict-side-1/grelmicro/sync/__init__.py b/.conflict-side-1/grelmicro/sync/__init__.py new file mode 100644 index 0000000..128d56c --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/__init__.py @@ -0,0 +1,6 @@ +"""Grelmicro Synchronization Primitives.""" + +from grelmicro.sync.leaderelection import LeaderElection +from grelmicro.sync.lock import Lock + +__all__ = ["LeaderElection", "Lock"] diff --git a/.conflict-side-1/grelmicro/sync/_backends.py b/.conflict-side-1/grelmicro/sync/_backends.py new file mode 100644 index 0000000..66f4b9f --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/_backends.py @@ -0,0 +1,30 @@ +"""Grelmicro Backend Registry. + +Contains loaded backends of each type to be used as default. + +Note: + For now, only lock backends are supported, but other backends may be added in the future. +""" + +from typing import Literal, NotRequired, TypedDict + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import BackendNotLoadedError + + +class LoadedBackendsDict(TypedDict): + """Loaded backends type.""" + + lock: NotRequired[SyncBackend] + + +loaded_backends: LoadedBackendsDict = {} + + +def get_sync_backend() -> SyncBackend: + """Get the lock backend.""" + backend: Literal["lock"] = "lock" + try: + return loaded_backends[backend] + except KeyError: + raise BackendNotLoadedError(backend) from None diff --git a/.conflict-side-1/grelmicro/sync/_base.py b/.conflict-side-1/grelmicro/sync/_base.py new file mode 100644 index 0000000..a0e6fb0 --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/_base.py @@ -0,0 +1,101 @@ +"""Grelmicro Lock API.""" + +from types import TracebackType +from typing import Annotated, Protocol, Self +from uuid import UUID + +from pydantic import BaseModel, ConfigDict +from typing_extensions import Doc + +from grelmicro.sync.abc import Synchronization + + +class BaseLockConfig(BaseModel): + """Base Lock Config.""" + + model_config = ConfigDict(frozen=True, extra="forbid") + + name: Annotated[ + str, + Doc(""" + The name of the resource to lock. + """), + ] + worker: Annotated[ + str | UUID, + Doc(""" + The worker identity. + + By default, use a UUIDv1. + """), + ] + + +class BaseLock(Synchronization, Protocol): + """Base Lock Protocol.""" + + async def __aenter__(self) -> Self: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + """ + ... + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + ... + + @property + def config(self) -> BaseLockConfig: + """Return the config.""" + ... + + async def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + ... + + async def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + WouldBlock: If the lock cannot be acquired without blocking. + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + ... + + async def release(self) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + ... + + async def locked(self) -> bool: + """Check if the lock is currently held.""" + ... + + async def owned(self) -> bool: + """Check if the lock is currently held by the current token.""" + ... diff --git a/.conflict-side-1/grelmicro/sync/_utils.py b/.conflict-side-1/grelmicro/sync/_utils.py new file mode 100644 index 0000000..2ad5dda --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/_utils.py @@ -0,0 +1,38 @@ +from threading import get_ident +from uuid import NAMESPACE_DNS, UUID, uuid3 + +from anyio import get_current_task + + +def generate_worker_namespace(worker: str) -> UUID: + """Generate a worker UUIDv3 namespace. + + Generate a worker UUID using UUIDv3 with the DNS namespace. + """ + return uuid3(namespace=NAMESPACE_DNS, name=worker) + + +def generate_task_token(worker: UUID | str) -> str: + """Generate a task UUID. + + The worker namespace is generated using `generate_worker_uuid` if the worker is a string. + Generate a task UUID using UUIDv3 with the worker namespace and the async task ID. + """ + worker = ( + generate_worker_namespace(worker) if isinstance(worker, str) else worker + ) + task = str(get_current_task().id) + return str(uuid3(namespace=worker, name=task)) + + +def generate_thread_token(worker: UUID | str) -> str: + """Generate a thread UUID. + + The worker namespace is generated using `generate_worker_uuid` if the worker is a string. + Generate a thread UUID using UUIDv3 with the worker namespace and the current thread ID. + """ + worker = ( + generate_worker_namespace(worker) if isinstance(worker, str) else worker + ) + thread = str(get_ident()) + return str(uuid3(namespace=worker, name=thread)) diff --git a/.conflict-side-1/grelmicro/sync/abc.py b/.conflict-side-1/grelmicro/sync/abc.py new file mode 100644 index 0000000..507477c --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/abc.py @@ -0,0 +1,106 @@ +"""Grelmicro Synchronization Abstract Base Classes and Protocols.""" + +from types import TracebackType +from typing import Protocol, Self, runtime_checkable + +from pydantic import PositiveFloat + + +class SyncBackend(Protocol): + """Synchronization Backend Protocol. + + This is the low level API for the distributed lock backend that is platform agnostic. + """ + + async def __aenter__(self) -> Self: + """Open the lock backend.""" + ... + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Close the lock backend.""" + ... + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock. + + Args: + name: The name of the lock. + token: The token to acquire the lock. + duration: The duration in seconds to hold the lock. + + Returns: + True if the lock is acquired, False if the lock is already acquired by another token. + + Raises: + Exception: Any exception can be raised if the lock cannot be acquired. + """ + ... + + async def release(self, *, name: str, token: str) -> bool: + """Release a lock. + + Args: + name: The name of the lock. + token: The token to release the lock. + + Returns: + True if the lock was released, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock cannot be released. + """ + ... + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired. + + Args: + name: The name of the lock. + + Returns: + True if the lock is acquired, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock status cannot be checked. + """ + ... + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned. + + Args: + name: The name of the lock. + token: The token to check. + + Returns: + True if the lock is owned by the token, False otherwise. + + Raises: + Exception: Any exception can be raised if the lock status cannot be checked. + """ + ... + + +@runtime_checkable +class Synchronization(Protocol): + """Synchronization Primitive Protocol.""" + + async def __aenter__(self) -> Self: + """Enter the synchronization primitive.""" + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the synchronization primitive.""" + ... + + +Seconds = PositiveFloat diff --git a/.conflict-side-1/grelmicro/sync/errors.py b/.conflict-side-1/grelmicro/sync/errors.py new file mode 100644 index 0000000..6384e36 --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/errors.py @@ -0,0 +1,67 @@ +"""Grelmicro Synchronization Primitive Errors.""" + +from grelmicro.errors import SettingsValidationError + + +class SyncError(Exception): + """Synchronization Primitive Error. + + This the base class for all lock errors. + """ + + +class SyncBackendError(SyncError): + """Synchronization Backend Error.""" + + +class BackendNotLoadedError(SyncBackendError): + """Backend Not Loaded Error.""" + + def __init__(self, backend_name: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not load backend {backend_name}, try initializing one first" + ) + + +class LockAcquireError(SyncBackendError): + """Acquire Lock Error. + + This error is raised when an error on backend side occurs during lock acquisition. + """ + + def __init__(self, *, name: str, token: str) -> None: + """Initialize the error.""" + super().__init__(f"Failed to acquire lock: name={name}, token={token}") + + +class LockReleaseError(SyncBackendError): + """Lock Release Error. + + This error is raised when an error on backend side occurs during lock release. + """ + + def __init__( + self, *, name: str, token: str, reason: str | None = None + ) -> None: + """Initialize the error.""" + super().__init__( + f"Failed to release lock: name={name}, token={token}" + + (f", reason={reason}" if reason else ""), + ) + + +class LockNotOwnedError(LockReleaseError): + """Lock Not Owned Error during Release. + + This error is raised when an attempt is made to release a lock that is not owned, respectively + the token is different or the lock is already expired. + """ + + def __init__(self, *, name: str, token: str) -> None: + """Initialize the error.""" + super().__init__(name=name, token=token, reason="lock not owned") + + +class SyncSettingsValidationError(SyncError, SettingsValidationError): + """Synchronization Settings Validation Error.""" diff --git a/.conflict-side-1/grelmicro/sync/leaderelection.py b/.conflict-side-1/grelmicro/sync/leaderelection.py new file mode 100644 index 0000000..62ce539 --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/leaderelection.py @@ -0,0 +1,386 @@ +"""Leader Election.""" + +from logging import getLogger +from time import monotonic +from types import TracebackType +from typing import TYPE_CHECKING, Annotated, Self +from uuid import UUID, uuid1 + +from anyio import ( + TASK_STATUS_IGNORED, + CancelScope, + Condition, + fail_after, + get_cancelled_exc_class, + move_on_after, + sleep, +) +from anyio.abc import TaskStatus +from pydantic import BaseModel, model_validator +from typing_extensions import Doc + +from grelmicro.sync._backends import get_sync_backend +from grelmicro.sync.abc import Seconds, SyncBackend, Synchronization +from grelmicro.task.abc import Task + +if TYPE_CHECKING: + from contextlib import AsyncExitStack + + from anyio.abc import TaskGroup + +logger = getLogger("grelmicro.leader_election") + + +class LeaderElectionConfig(BaseModel): + """Leader Election Config. + + Leader election based on a leased reentrant distributed lock. + """ + + name: Annotated[ + str, + Doc( + """ + The leader election lock name. + """, + ), + ] + worker: Annotated[ + str | UUID, + Doc( + """ + The worker identity used as lock token. + """, + ), + ] + lease_duration: Annotated[ + Seconds, + Doc( + """ + The lease duration in seconds. + """, + ), + ] = 15 + renew_deadline: Annotated[ + Seconds, + Doc( + """ + The renew deadline in seconds. + """, + ), + ] = 10 + retry_interval: Annotated[ + Seconds, + Doc( + """ + The retry interval in seconds. + """, + ), + ] = 2 + backend_timeout: Annotated[ + Seconds, + Doc( + """ + The backend timeout in seconds. + """, + ), + ] = 5 + error_interval: Annotated[ + Seconds, + Doc( + """ + The error interval in seconds. + """, + ), + ] = 30 + + @model_validator(mode="after") + def _validate(self) -> Self: + if self.renew_deadline >= self.lease_duration: + msg = "Renew deadline must be shorter than lease duration" + raise ValueError(msg) + if self.retry_interval >= self.renew_deadline: + msg = "Retry interval must be shorter than renew deadline" + raise ValueError(msg) + if self.backend_timeout >= self.renew_deadline: + msg = "Backend timeout must be shorter than renew deadline" + raise ValueError(msg) + return self + + +class LeaderElection(Synchronization, Task): + """Leader Election. + + The leader election is a synchronization primitive with the worker as scope. + It runs as a task to acquire or renew the distributed lock. + """ + + def __init__( + self, + name: Annotated[ + str, + Doc( + """ + The name of the resource representing the leader election. + + It will be used as the lock name so make sure it is unique on the distributed lock + backend. + """, + ), + ], + *, + backend: Annotated[ + SyncBackend | None, + Doc( + """ + The distributed lock backend used to acquire and release the lock. + + By default, it will use the lock backend registry to get the default lock backend. + """, + ), + ] = None, + worker: Annotated[ + str | UUID | None, + Doc( + """ + The worker identity. + + By default, use a UUIDv1 will be generated. + """, + ), + ] = None, + lease_duration: Annotated[ + Seconds, + Doc( + """ + The duration in seconds after the lock will be released if not renewed. + + If the worker becomes unavailable, the lock can only be acquired by an other worker + after it' has expired. + """, + ), + ] = 15, + renew_deadline: Annotated[ + Seconds, + Doc( + """ + The duration in seconds that the leader worker will try to acquire the lock before + giving up. + + Must be shorter than the lease duration. In case of multiple failures, the leader + worker will loose the lead to prevent split-brain scenarios and ensure that only one + worker is the leader at any time. + """, + ), + ] = 10, + retry_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between attempts to acquire or renew the lock. + + Must be shorter than the renew deadline. A shorter schedule enables faster leader + elections but may increase load on the distributed lock backend, while a longer + schedule reduces load but can delay new leader elections. + """, + ), + ] = 2, + backend_timeout: Annotated[ + Seconds, + Doc( + """ + The duration in seconds for waiting on backend for acquiring and releasing the lock. + + This value determines how long the system will wait before giving up the current + operation. + """, + ), + ] = 5, + error_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between logging error messages. + + If shorter than the retry interval, it will log every error. It is used to prevent + flooding the logs when the lock backend is unavailable. + """, + ), + ] = 30, + ) -> None: + """Initialize the leader election.""" + self.config = LeaderElectionConfig( + name=name, + worker=worker or uuid1(), + lease_duration=lease_duration, + renew_deadline=renew_deadline, + retry_interval=retry_interval, + backend_timeout=backend_timeout, + error_interval=error_interval, + ) + self.backend = backend or get_sync_backend() + + self._service_running = False + self._state_change_condition: Condition = Condition() + self._is_leader: bool = False + self._state_updated_at: float = monotonic() + self._error_logged_at: float | None = None + self._task_group: TaskGroup | None = None + self._exit_stack: AsyncExitStack | None = None + + async def __aenter__(self) -> Self: + """Wait for the leader with the context manager.""" + await self.wait_for_leader() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the context manager.""" + + @property + def name(self) -> str: + """Return the task name.""" + return self.config.name + + def is_running(self) -> bool: + """Check if the leader election task is running.""" + return self._service_running + + def is_leader(self) -> bool: + """Check if the current worker is the leader. + + To avoid a split-brain scenario, the leader considers itself as no longer leader if the + renew deadline is reached. + + Returns: + True if the current worker is the leader, False otherwise. + + """ + if not self._is_leader: + return False + return not self._is_renew_deadline_reached() + + async def wait_for_leader(self) -> None: + """Wait until the current worker is the leader.""" + while not self.is_leader(): + async with self._state_change_condition: + await self._state_change_condition.wait() + + async def wait_lose_leader(self) -> None: + """Wait until the current worker is no longer the leader.""" + while self.is_leader(): + with move_on_after(self._seconds_before_expiration_deadline()): + async with self._state_change_condition: + await self._state_change_condition.wait() + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run polling loop service to acquire or renew the distributed lock.""" + task_status.started() + if self._service_running: + logger.warning("Leader Election already running: %s", self.name) + return + self._service_running = True + logger.info("Leader Election started: %s", self.name) + try: + while True: + await self._try_acquire_or_renew() + await sleep(self.config.retry_interval) + except get_cancelled_exc_class(): + logger.info("Leader Election stopped: %s", self.name) + raise + except BaseException: + logger.exception("Leader Election crashed: %s", self.name) + raise + finally: + self._service_running = False + with CancelScope(shield=True): + await self._release() + + async def _update_state( + self, *, is_leader: bool, raison_if_no_more_leader: str + ) -> None: + """Update the state of the leader election.""" + self._state_updated_at = monotonic() + if is_leader is self._is_leader: + return # No change + + self._is_leader = is_leader + + if is_leader: + logger.info("Leader Election acquired leadership: %s", self.name) + else: + logger.warning( + "Leader Election lost leadership: %s (%s)", + self.name, + raison_if_no_more_leader, + ) + + async with self._state_change_condition: + self._state_change_condition.notify_all() + + async def _try_acquire_or_renew(self) -> None: + """Try to acquire leadership.""" + try: + with fail_after(self.config.backend_timeout): + is_leader = await self.backend.acquire( + name=self.name, + token=str(self.config.worker), + duration=self.config.lease_duration, + ) + except Exception: + if self._check_error_interval(): + logger.exception( + "Leader Election failed to acquire lock: %s", self.name + ) + if self._is_renew_deadline_reached(): + await self._update_state( + is_leader=False, + raison_if_no_more_leader="renew deadline reached", + ) + else: + await self._update_state( + is_leader=is_leader, + raison_if_no_more_leader="lock not acquired", + ) + + def _seconds_before_expiration_deadline(self) -> float: + return max( + self._state_updated_at + self.config.lease_duration - monotonic(), 0 + ) + + def _check_error_interval(self) -> bool: + """Check if the cooldown interval allows to log the error.""" + is_logging_allowed = ( + not self._error_logged_at + or (monotonic() - self._error_logged_at) + > self.config.error_interval + ) + self._error_logged_at = monotonic() + return is_logging_allowed + + def _is_renew_deadline_reached(self) -> bool: + return ( + monotonic() - self._state_updated_at + ) >= self.config.renew_deadline + + async def _release(self) -> None: + try: + with fail_after(self.config.backend_timeout): + if not ( + await self.backend.release( + name=self.config.name, token=str(self.config.worker) + ) + ): + logger.info( + "Leader Election lock already released: %s", self.name + ) + except Exception: + logger.exception( + "Leader Election failed to release lock: %s", self.name + ) diff --git a/.conflict-side-1/grelmicro/sync/lock.py b/.conflict-side-1/grelmicro/sync/lock.py new file mode 100644 index 0000000..c87d08f --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/lock.py @@ -0,0 +1,324 @@ +"""Grelmicro Lock.""" + +from time import sleep as thread_sleep +from types import TracebackType +from typing import Annotated, Self +from uuid import UUID, uuid1 + +from anyio import WouldBlock, from_thread, sleep +from typing_extensions import Doc + +from grelmicro.sync._backends import get_sync_backend +from grelmicro.sync._base import BaseLock, BaseLockConfig +from grelmicro.sync._utils import generate_task_token, generate_thread_token +from grelmicro.sync.abc import Seconds, SyncBackend +from grelmicro.sync.errors import ( + LockAcquireError, + LockNotOwnedError, + LockReleaseError, + SyncBackendError, +) + + +class LockConfig(BaseLockConfig, frozen=True, extra="forbid"): + """Lock Config.""" + + lease_duration: Annotated[ + Seconds, + Doc( + """ + The lease duration in seconds for the lock. + """, + ), + ] + retry_interval: Annotated[ + Seconds, + Doc( + """ + The interval in seconds between attempts to acquire the lock. + """, + ), + ] + + +class Lock(BaseLock): + """Lock. + + This lock is a distributed lock that is used to acquire a resource across multiple workers. The + lock is acquired asynchronously and can be extended multiple times manually. The lock is + automatically released after a duration if not extended. + """ + + def __init__( + self, + name: Annotated[ + str, + Doc( + """ + The name of the resource to lock. + + It will be used as the lock name so make sure it is unique on the lock backend. + """, + ), + ], + *, + backend: Annotated[ + SyncBackend | None, + Doc(""" + The distributed lock backend used to acquire and release the lock. + + By default, it will use the lock backend registry to get the default lock backend. + """), + ] = None, + worker: Annotated[ + str | UUID | None, + Doc( + """ + The worker identity. + + By default, use a UUIDv1 will be generated. + """, + ), + ] = None, + lease_duration: Annotated[ + Seconds, + Doc( + """ + The duration in seconds for the lock to be held by default. + """, + ), + ] = 60, + retry_interval: Annotated[ + Seconds, + Doc( + """ + The duration in seconds between attempts to acquire the lock. + + Should be greater or equal than 0.1 to prevent flooding the lock backend. + """, + ), + ] = 0.1, + ) -> None: + """Initialize the lock.""" + self._config: LockConfig = LockConfig( + name=name, + worker=worker or uuid1(), + lease_duration=lease_duration, + retry_interval=retry_interval, + ) + self.backend = backend or get_sync_backend() + self._from_thread: ThreadLockAdapter | None = None + + async def __aenter__(self) -> Self: + """Acquire the lock with the async context manager.""" + await self.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock with the async context manager. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + await self.release() + + @property + def config(self) -> LockConfig: + """Return the lock config.""" + return self._config + + @property + def from_thread(self) -> "ThreadLockAdapter": + """Return the lock adapter for worker thread.""" + if self._from_thread is None: + self._from_thread = ThreadLockAdapter(lock=self) + return self._from_thread + + async def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + while not await self.do_acquire(token=token): # noqa: ASYNC110 // Polling is intentional + await sleep(self._config.retry_interval) + + async def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + WouldBlock: If the lock cannot be acquired without blocking. + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + if not await self.do_acquire(token=token): + msg = f"Lock not acquired: name={self._config.name}, token={token}" + raise WouldBlock(msg) + + async def release(self) -> None: + """Release the lock. + + Raises: + LockNotOwnedError: If the lock is not owned by the current token. + LockReleaseError: If the lock cannot be released due to an error on the backend. + + """ + token = generate_task_token(self._config.worker) + if not await self.do_release(token): + raise LockNotOwnedError(name=self._config.name, token=token) + + async def locked(self) -> bool: + """Check if the lock is acquired. + + Raise: + SyncBackendError: If the lock cannot be checked due to an error on the backend. + """ + try: + return await self.backend.locked(name=self._config.name) + except Exception as exc: + msg = "Failed to check if the lock is acquired" + raise SyncBackendError(msg) from exc + + async def owned(self) -> bool: + """Check if the lock is owned by the current token. + + Raise: + SyncBackendError: If the lock cannot be checked due to an error on the backend. + """ + return await self.do_owned(generate_task_token(self._config.worker)) + + async def do_acquire(self, token: str) -> bool: + """Acquire the lock. + + This method should not be called directly. Use `acquire` instead. + + Returns: + bool: True if the lock was acquired, False if the lock was not acquired. + + Raises: + LockAcquireError: If the lock cannot be acquired due to an error on the backend. + """ + try: + return await self.backend.acquire( + name=self._config.name, + token=token, + duration=self._config.lease_duration, + ) + except Exception as exc: + raise LockAcquireError(name=self._config.name, token=token) from exc + + async def do_release(self, token: str) -> bool: + """Release the lock. + + This method should not be called directly. Use `release` instead. + + Returns: + bool: True if the lock was released, False otherwise. + + Raises: + LockReleaseError: Cannot release the lock due to backend error. + """ + try: + return await self.backend.release( + name=self._config.name, token=token + ) + except Exception as exc: + raise LockReleaseError(name=self._config.name, token=token) from exc + + async def do_owned(self, token: str) -> bool: + """Check if the lock is owned by the current token. + + This method should not be called directly. Use `owned` instead. + + Returns: + bool: True if the lock is owned by the current token, False otherwise. + + Raises: + SyncBackendError: Cannot check if the lock is owned due to backend error. + """ + try: + return await self.backend.owned(name=self._config.name, token=token) + except Exception as exc: + msg = "Failed to check if the lock is owned" + raise SyncBackendError(msg) from exc + + +class ThreadLockAdapter: + """Lock Adapter for Worker Thread.""" + + def __init__(self, lock: Lock) -> None: + """Initialize the lock adapter.""" + self._lock = lock + + def __enter__(self) -> Self: + """Acquire the lock with the context manager.""" + self.acquire() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Release the lock with the context manager.""" + self.release() + + def acquire(self) -> None: + """Acquire the lock. + + Raises: + LockAcquireError: Cannot acquire the lock due to backend error. + + """ + token = generate_thread_token(self._lock.config.worker) + retry_interval = self._lock.config.retry_interval + while not from_thread.run(self._lock.do_acquire, token): + thread_sleep(retry_interval) + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + Raises: + LockAcquireError: Cannot acquire the lock due to backend error. + WouldBlock: If the lock cannot be acquired without blocking. + + """ + token = generate_thread_token(self._lock.config.worker) + if not from_thread.run(self._lock.do_acquire, token): + msg = f"Lock not acquired: name={self._lock.config.name}, token={token}" + raise WouldBlock(msg) + + def release(self) -> None: + """Release the lock. + + Raises: + ReleaseSyncBackendError: Cannot release the lock due to backend error. + LockNotOwnedError: If the lock is not currently held. + + """ + token = generate_thread_token(self._lock.config.worker) + if not from_thread.run(self._lock.do_release, token): + raise LockNotOwnedError(name=self._lock.config.name, token=token) + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + return from_thread.run(self._lock.locked) + + def owned(self) -> bool: + """Return True if the lock is currently held by the current worker thread.""" + return from_thread.run( + self._lock.do_owned, generate_thread_token(self._lock.config.worker) + ) diff --git a/.conflict-side-1/grelmicro/sync/memory.py b/.conflict-side-1/grelmicro/sync/memory.py new file mode 100644 index 0000000..9746c59 --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/memory.py @@ -0,0 +1,78 @@ +"""Memory Synchronization Backend.""" + +from time import monotonic +from types import TracebackType +from typing import Annotated, Self + +from typing_extensions import Doc + +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend + + +class MemorySyncBackend(SyncBackend): + """Memory Synchronization Backend. + + This is not a backend with a real distributed lock. It is a local lock that can be used for + testing purposes or for locking operations that are executed in the same AnyIO event loop. + """ + + def __init__( + self, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + ) -> None: + """Initialize the lock backend.""" + self._locks: dict[str, tuple[str | None, float]] = {} + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Enter the lock backend.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Exit the lock backend.""" + self._locks.clear() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + if ( + current_token is None + or current_token == token + or expire_at < monotonic() + ): + self._locks[name] = (token, monotonic() + duration) + return True + return False + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + if current_token == token and expire_at >= monotonic(): + del self._locks[name] + return True + if current_token and expire_at < monotonic(): + del self._locks[name] + return False + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + return current_token is not None and expire_at >= monotonic() + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + current_token, expire_at = self._locks.get(name, (None, 0)) + return current_token == token and expire_at >= monotonic() diff --git a/.conflict-side-1/grelmicro/sync/postgres.py b/.conflict-side-1/grelmicro/sync/postgres.py new file mode 100644 index 0000000..451cc0c --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/postgres.py @@ -0,0 +1,206 @@ +"""PostgreSQL Synchronization Backend.""" + +from types import TracebackType +from typing import Annotated, Self + +from asyncpg import Pool, create_pool +from pydantic import PostgresDsn +from pydantic_core import MultiHostUrl, ValidationError +from pydantic_settings import BaseSettings +from typing_extensions import Doc + +from grelmicro.errors import OutOfContextError +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import SyncSettingsValidationError + + +class _PostgresSettings(BaseSettings): + """PostgreSQL settings from the environment variables.""" + + POSTGRES_HOST: str | None = None + POSTGRES_PORT: int = 5432 + POSTGRES_DB: str | None = None + POSTGRES_USER: str | None = None + POSTGRES_PASSWORD: str | None = None + POSTGRES_URL: PostgresDsn | None = None + + +def _get_postgres_url() -> str: + """Get the PostgreSQL URL from the environment variables. + + Raises: + SyncSettingsValidationError: If the URL or all of the host, database, user, and password + """ + try: + settings = _PostgresSettings() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + required_parts = [ + settings.POSTGRES_HOST, + settings.POSTGRES_DB, + settings.POSTGRES_USER, + settings.POSTGRES_PASSWORD, + ] + + if settings.POSTGRES_URL and not any(required_parts): + return settings.POSTGRES_URL.unicode_string() + + if all(required_parts) and not settings.POSTGRES_URL: + return MultiHostUrl.build( + scheme="postgresql", + username=settings.POSTGRES_USER, + password=settings.POSTGRES_PASSWORD, + host=settings.POSTGRES_HOST, + port=settings.POSTGRES_PORT, + path=settings.POSTGRES_DB, + ).unicode_string() + + msg = ( + "Either POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and " + "POSTGRES_PASSWORD must be set" + ) + raise SyncSettingsValidationError(msg) + + +class PostgresSyncBackend(SyncBackend): + """PostgreSQL Synchronization Backend.""" + + _SQL_CREATE_TABLE_IF_NOT_EXISTS = """ + CREATE TABLE IF NOT EXISTS {table_name} ( + name TEXT PRIMARY KEY, + token TEXT NOT NULL, + expire_at TIMESTAMP NOT NULL + ); + """ + + _SQL_ACQUIRE_OR_EXTEND = """ + INSERT INTO {table_name} (name, token, expire_at) + VALUES ($1, $2, NOW() + make_interval(secs => $3)) + ON CONFLICT (name) DO UPDATE + SET token = EXCLUDED.token, expire_at = EXCLUDED.expire_at + WHERE {table_name}.token = EXCLUDED.token OR {table_name}.expire_at < NOW() + RETURNING 1; + """ + + _SQL_RELEASE = """ + DELETE FROM {table_name} + WHERE name = $1 AND token = $2 AND expire_at >= NOW() + RETURNING 1; + """ + + _SQL_RELEASE_ALL_EXPIRED = """ + DELETE FROM {table_name} + WHERE expire_at < NOW(); + """ + + _SQL_LOCKED = """ + SELECT 1 FROM {table_name} + WHERE name = $1 AND expire_at >= NOW(); + """ + + _SQL_OWNED = """ + SELECT 1 FROM {table_name} + WHERE name = $1 AND token = $2 AND expire_at >= NOW(); + """ + + def __init__( + self, + url: Annotated[ + PostgresDsn | str | None, + Doc(""" + The Postgres database URL. + + If not provided, the URL will be taken from the environment variables POSTGRES_URL + or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD. + """), + ] = None, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + table_name: Annotated[ + str, Doc("The table name to store the locks.") + ] = "locks", + ) -> None: + """Initialize the lock backend.""" + if not table_name.isidentifier(): + msg = f"Table name '{table_name}' is not a valid identifier" + raise ValueError(msg) + + self._url = url or _get_postgres_url() + self._table_name = table_name + self._acquire_sql = self._SQL_ACQUIRE_OR_EXTEND.format( + table_name=table_name + ) + self._release_sql = self._SQL_RELEASE.format(table_name=table_name) + self._pool: Pool | None = None + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Enter the lock backend.""" + self._pool = await create_pool(str(self._url)) + await self._pool.execute( + self._SQL_CREATE_TABLE_IF_NOT_EXISTS.format( + table_name=self._table_name + ), + ) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Exit the lock backend.""" + if self._pool: + await self._pool.execute( + self._SQL_RELEASE_ALL_EXPIRED.format( + table_name=self._table_name + ), + ) + await self._pool.close() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire a lock.""" + if not self._pool: + raise OutOfContextError(self, "acquire") + + return bool( + await self._pool.fetchval(self._acquire_sql, name, token, duration) + ) + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + if not self._pool: + raise OutOfContextError(self, "release") + return bool(await self._pool.fetchval(self._release_sql, name, token)) + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + if not self._pool: + raise OutOfContextError(self, "locked") + return bool( + await self._pool.fetchval( + self._SQL_LOCKED.format(table_name=self._table_name), + name, + ), + ) + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + if not self._pool: + raise OutOfContextError(self, "owned") + return bool( + await self._pool.fetchval( + self._SQL_OWNED.format(table_name=self._table_name), + name, + token, + ), + ) diff --git a/.conflict-side-1/grelmicro/sync/redis.py b/.conflict-side-1/grelmicro/sync/redis.py new file mode 100644 index 0000000..73090c8 --- /dev/null +++ b/.conflict-side-1/grelmicro/sync/redis.py @@ -0,0 +1,146 @@ +"""Redis Synchronization Backend.""" + +from types import TracebackType +from typing import Annotated, Self + +from pydantic import RedisDsn, ValidationError +from pydantic_core import Url +from pydantic_settings import BaseSettings +from redis.asyncio.client import Redis +from typing_extensions import Doc + +from grelmicro.sync._backends import loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import SyncSettingsValidationError + + +class _RedisSettings(BaseSettings): + """Redis settings from the environment variables.""" + + REDIS_HOST: str | None = None + REDIS_PORT: int = 6379 + REDIS_DB: int = 0 + REDIS_PASSWORD: str | None = None + REDIS_URL: RedisDsn | None = None + + +def _get_redis_url() -> str: + """Get the Redis URL from the environment variables. + + Raises: + SyncSettingsValidationError: If the URL or host is not set. + """ + try: + settings = _RedisSettings() + except ValidationError as error: + raise SyncSettingsValidationError(error) from None + + if settings.REDIS_URL and not settings.REDIS_HOST: + return settings.REDIS_URL.unicode_string() + + if settings.REDIS_HOST and not settings.REDIS_URL: + return Url.build( + scheme="redis", + host=settings.REDIS_HOST, + port=settings.REDIS_PORT, + path=str(settings.REDIS_DB), + password=settings.REDIS_PASSWORD, + ).unicode_string() + + msg = "Either REDIS_URL or REDIS_HOST must be set" + raise SyncSettingsValidationError(msg) + + +class RedisSyncBackend(SyncBackend): + """Redis Synchronization Backend.""" + + _LUA_ACQUIRE_OR_EXTEND = """ + local token = redis.call('get', KEYS[1]) + if not token then + redis.call('set', KEYS[1], ARGV[1], 'px', ARGV[2]) + return 1 + end + if token == ARGV[1] then + redis.call('pexpire', KEYS[1], ARGV[2]) + return 1 + end + return 0 + """ + _LUA_RELEASE = """ + local token = redis.call('get', KEYS[1]) + if not token or token ~= ARGV[1] then + return 0 + end + redis.call('del', KEYS[1]) + return 1 + """ + + def __init__( + self, + url: Annotated[ + RedisDsn | str | None, + Doc(""" + The Redis URL. + + If not provided, the URL will be taken from the environment variables REDIS_URL + or REDIS_HOST, REDIS_PORT, REDIS_DB, and REDIS_PASSWORD. + """), + ] = None, + *, + auto_register: Annotated[ + bool, + Doc( + "Automatically register the lock backend in the backend registry." + ), + ] = True, + ) -> None: + """Initialize the lock backend.""" + self._url = url or _get_redis_url() + self._redis: Redis = Redis.from_url(str(self._url)) + self._lua_release = self._redis.register_script(self._LUA_RELEASE) + self._lua_acquire = self._redis.register_script( + self._LUA_ACQUIRE_OR_EXTEND + ) + if auto_register: + loaded_backends["lock"] = self + + async def __aenter__(self) -> Self: + """Open the lock backend.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + """Close the lock backend.""" + await self._redis.aclose() + + async def acquire(self, *, name: str, token: str, duration: float) -> bool: + """Acquire the lock.""" + return bool( + await self._lua_acquire( + keys=[name], + args=[token, int(duration * 1000)], + client=self._redis, + ) + ) + + async def release(self, *, name: str, token: str) -> bool: + """Release the lock.""" + return bool( + await self._lua_release( + keys=[name], args=[token], client=self._redis + ) + ) + + async def locked(self, *, name: str) -> bool: + """Check if the lock is acquired.""" + return bool(await self._redis.get(name)) + + async def owned(self, *, name: str, token: str) -> bool: + """Check if the lock is owned.""" + return bool( + (await self._redis.get(name)) == token.encode() + ) # redis returns bytes diff --git a/.conflict-side-1/grelmicro/task/__init__.py b/.conflict-side-1/grelmicro/task/__init__.py new file mode 100644 index 0000000..374bf08 --- /dev/null +++ b/.conflict-side-1/grelmicro/task/__init__.py @@ -0,0 +1,6 @@ +"""Grelmicro Task Scheduler.""" + +from grelmicro.task.manager import TaskManager +from grelmicro.task.router import TaskRouter + +__all__ = ["TaskManager", "TaskRouter"] diff --git a/.conflict-side-1/grelmicro/task/_interval.py b/.conflict-side-1/grelmicro/task/_interval.py new file mode 100644 index 0000000..f66c2f2 --- /dev/null +++ b/.conflict-side-1/grelmicro/task/_interval.py @@ -0,0 +1,92 @@ +"""Interval Task.""" + +from collections.abc import Awaitable, Callable +from contextlib import nullcontext +from functools import partial +from inspect import iscoroutinefunction +from logging import getLogger +from typing import Any + +from anyio import TASK_STATUS_IGNORED, sleep, to_thread +from anyio.abc import TaskStatus +from fast_depends import inject + +from grelmicro.sync.abc import Synchronization +from grelmicro.task._utils import validate_and_generate_reference +from grelmicro.task.abc import Task + +logger = getLogger("grelmicro.task") + + +class IntervalTask(Task): + """Interval Task. + + Use the `TaskManager.interval()` or `SchedulerRouter.interval()` decorator instead + of creating IntervalTask objects directly. + """ + + def __init__( + self, + *, + function: Callable[..., Any], + name: str | None = None, + interval: float, + sync: Synchronization | None = None, + ) -> None: + """Initialize the IntervalTask. + + Raises: + FunctionNotSupportedError: If the function is not supported. + ValueError: If internal is less than or equal to 0. + """ + if interval <= 0: + msg = "Interval must be greater than 0" + raise ValueError(msg) + + alt_name = validate_and_generate_reference(function) + self._name = name or alt_name + self._interval = interval + self._async_function = self._prepare_async_function(function) + self._sync = sync if sync else nullcontext() + + @property + def name(self) -> str: + """Return the lock name.""" + return self._name + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run the repeated task loop.""" + logger.info( + "Task started (interval: %ss): %s", self._interval, self.name + ) + task_status.started() + try: + while True: + try: + async with self._sync: + try: + await self._async_function() + except Exception: + logger.exception( + "Task execution error: %s", self.name + ) + except Exception: + logger.exception( + "Task synchronization error: %s", self.name + ) + await sleep(self._interval) + finally: + logger.info("Task stopped: %s", self.name) + + def _prepare_async_function( + self, function: Callable[..., Any] + ) -> Callable[..., Awaitable[Any]]: + """Prepare the function with lock and ensure async function.""" + function = inject(function) + return ( + function + if iscoroutinefunction(function) + else partial(to_thread.run_sync, function) + ) diff --git a/.conflict-side-1/grelmicro/task/_utils.py b/.conflict-side-1/grelmicro/task/_utils.py new file mode 100644 index 0000000..7cfec3f --- /dev/null +++ b/.conflict-side-1/grelmicro/task/_utils.py @@ -0,0 +1,43 @@ +"""Task Utilities.""" + +from collections.abc import Callable +from functools import partial +from inspect import ismethod +from typing import Any + +from grelmicro.task.errors import FunctionTypeError + + +def validate_and_generate_reference(function: Callable[..., Any]) -> str: + """Generate a task name from the given function. + + This implementation is inspirated by the APScheduler project under MIT License. + Original source: https://github.com/agronholm/apscheduler/blob/master/src/apscheduler/_marshalling.py + + Raises: + FunctionNotSupportedError: If function is not supported. + + """ + if isinstance(function, partial): + ref = "partial()" + raise FunctionTypeError(ref) + + if ismethod(function): + ref = "method" + raise FunctionTypeError(ref) + + if not hasattr(function, "__module__") or not hasattr( + function, "__qualname__" + ): + ref = "callable without __module__ or __qualname__ attribute" + raise FunctionTypeError(ref) + + if "" in function.__qualname__: + ref = "lambda" + raise FunctionTypeError(ref) + + if "" in function.__qualname__: + ref = "nested function" + raise FunctionTypeError(ref) + + return f"{function.__module__}:{function.__qualname__}" diff --git a/.conflict-side-1/grelmicro/task/abc.py b/.conflict-side-1/grelmicro/task/abc.py new file mode 100644 index 0000000..d4e7cf3 --- /dev/null +++ b/.conflict-side-1/grelmicro/task/abc.py @@ -0,0 +1,31 @@ +"""Grelmicro Task Synchronization Abstract Base Classes and Protocols.""" + +from typing import Protocol + +from anyio import TASK_STATUS_IGNORED +from anyio.abc import TaskStatus +from typing_extensions import runtime_checkable + + +@runtime_checkable +class Task(Protocol): + """Task Protocol. + + A task that runs in background in the async event loop. + """ + + @property + def name(self) -> str: + """Name to uniquely identify the task.""" + ... + + async def __call__( + self, + *, + task_status: TaskStatus[None] = TASK_STATUS_IGNORED, + ) -> None: + """Run the task. + + This is the entry point of the task to be run in the async event loop. + """ + ... diff --git a/.conflict-side-1/grelmicro/task/errors.py b/.conflict-side-1/grelmicro/task/errors.py new file mode 100644 index 0000000..a788f61 --- /dev/null +++ b/.conflict-side-1/grelmicro/task/errors.py @@ -0,0 +1,28 @@ +"""Grelmicro Task Scheduler Errors.""" + +from grelmicro.errors import GrelmicroError + + +class TaskError(GrelmicroError): + """Base Grelmicro Task error.""" + + +class FunctionTypeError(TaskError, TypeError): + """Function Type Error.""" + + def __init__(self, reference: str) -> None: + """Initialize the error.""" + super().__init__( + f"Could not use function {reference}, " + "try declaring 'def' or 'async def' directly in the module" + ) + + +class TaskAddOperationError(TaskError, RuntimeError): + """Task Add Operation Error.""" + + def __init__(self) -> None: + """Initialize the error.""" + super().__init__( + "Could not add the task, try calling 'add_task' and 'include_router' before starting" + ) diff --git a/.conflict-side-1/grelmicro/task/manager.py b/.conflict-side-1/grelmicro/task/manager.py new file mode 100644 index 0000000..5432145 --- /dev/null +++ b/.conflict-side-1/grelmicro/task/manager.py @@ -0,0 +1,89 @@ +"""Grelmicro Task Manager.""" + +from contextlib import AsyncExitStack +from logging import getLogger +from types import TracebackType +from typing import TYPE_CHECKING, Annotated, Self + +from anyio import create_task_group +from typing_extensions import Doc + +from grelmicro.errors import OutOfContextError +from grelmicro.task.abc import Task +from grelmicro.task.errors import TaskAddOperationError +from grelmicro.task.router import TaskRouter + +if TYPE_CHECKING: + from anyio.abc import TaskGroup + +logger = getLogger("grelmicro.task") + + +class TaskManager(TaskRouter): + """Task Manager. + + `TaskManager` class, the main entrypoint to manage scheduled tasks. + """ + + def __init__( + self, + *, + auto_start: Annotated[ + bool, + Doc( + """ + Automatically start all tasks. + """, + ), + ] = True, + tasks: Annotated[ + list[Task] | None, + Doc( + """ + A list of tasks to be started. + """, + ), + ] = None, + ) -> None: + """Initialize the task manager.""" + TaskRouter.__init__(self, tasks=tasks) + + self._auto_start = auto_start + self._task_group: TaskGroup | None = None + + async def __aenter__(self) -> Self: + """Enter the context manager.""" + self._exit_stack = AsyncExitStack() + await self._exit_stack.__aenter__() + self._task_group = await self._exit_stack.enter_async_context( + create_task_group(), + ) + if self._auto_start: + await self.start() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the context manager.""" + if not self._task_group or not self._exit_stack: + raise OutOfContextError(self, "__aexit__") + self._task_group.cancel_scope.cancel() + return await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb) + + async def start(self) -> None: + """Start all tasks manually.""" + if not self._task_group: + raise OutOfContextError(self, "start") + + if self._started: + raise TaskAddOperationError + + self.do_mark_as_started() + + for task in self.tasks: + await self._task_group.start(task.__call__) + logger.debug("%s scheduled tasks started", len(self._tasks)) diff --git a/.conflict-side-1/grelmicro/task/router.py b/.conflict-side-1/grelmicro/task/router.py new file mode 100644 index 0000000..16b240d --- /dev/null +++ b/.conflict-side-1/grelmicro/task/router.py @@ -0,0 +1,132 @@ +"""Grelmicro Task Router.""" + +from collections.abc import Awaitable, Callable +from typing import Annotated, Any + +from typing_extensions import Doc + +from grelmicro.sync.abc import Synchronization +from grelmicro.task.abc import Task +from grelmicro.task.errors import TaskAddOperationError + + +class TaskRouter: + """Task Router. + + `TaskRouter` class, used to group task schedules, for example to structure an app in + multiple files. It would then included in the `TaskManager`, or in another + `TaskRouter`. + """ + + def __init__( + self, + *, + tasks: Annotated[ + list[Task] | None, + Doc( + """ + A list of schedules or scheduled tasks to be scheduled. + """, + ), + ] = None, + ) -> None: + """Initialize the task router.""" + self._started = False + self._tasks: list[Task] = tasks or [] + self._routers: list[TaskRouter] = [] + + @property + def tasks(self) -> list[Task]: + """List of scheduled tasks.""" + return self._tasks + [ + task for router in self._routers for task in router.tasks + ] + + def add_task(self, task: Task) -> None: + """Add a task to the scheduler.""" + if self._started: + raise TaskAddOperationError + + self._tasks.append(task) + + def interval( + self, + *, + seconds: Annotated[ + float, + Doc( + """ + The duration in seconds between each task run. + + Accuracy is not guaranteed and may vary with system load. Consider the + execution time of the task when setting the interval. + """, + ), + ], + name: Annotated[ + str | None, + Doc( + """ + The name of the task. + + If None, a name will be generated automatically from the function. + """, + ), + ] = None, + sync: Annotated[ + Synchronization | None, + Doc( + """ + The synchronization primitive to use for the task. + + You can use a `LeasedLock` or a `LeaderElection`, for example. If None, + no synchronization is used and the task will run on all workers. + """, + ), + ] = None, + ) -> Callable[ + [Callable[..., Any | Awaitable[Any]]], + Callable[..., Any | Awaitable[Any]], + ]: + """Decorate function to add it to the task scheduler. + + Raises: + TaskNameGenerationError: If the task name generation fails. + """ + from grelmicro.task._interval import IntervalTask + + def decorator( + function: Callable[[], None | Awaitable[None]], + ) -> Callable[[], None | Awaitable[None]]: + self.add_task( + IntervalTask( + name=name, + function=function, + interval=seconds, + sync=sync, + ), + ) + return function + + return decorator + + def include_router(self, router: "TaskRouter") -> None: + """Include another router in this router.""" + if self._started: + raise TaskAddOperationError + + self._routers.append(router) + + def started(self) -> bool: + """Check if the task manager has started.""" + return self._started + + def do_mark_as_started(self) -> None: + """Mark the task manager as started. + + Do not call this method directly. It is called by the task manager when the task + manager is started. + """ + self._started = True + for router in self._routers: + router.do_mark_as_started() diff --git a/.conflict-side-1/mkdocs.yml b/.conflict-side-1/mkdocs.yml new file mode 100644 index 0000000..0b08e9f --- /dev/null +++ b/.conflict-side-1/mkdocs.yml @@ -0,0 +1,47 @@ +site_name: Grelmicro +site_description: Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. +site_url: https://grelmicro.grel.info +theme: + name: material + palette: + primary: green + accent: light green + font: + text: 'Roboto' + code: 'Roboto Mono' + features: + - content.tabs.link + - content.code.copy + - content.code.select + - content.tooltips + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.progress + - navigation.top + - navigation.tracking + +repo_name: grelinfo/grelmicro +repo_url: https://github.com/grelinfo/grelmicro + +validation: + omitted_files: warn + absolute_links: warn + unrecognized_links: warn + +nav: +- Grelmicro: index.md +- User Guide: + - logging.md + - sync.md + - task.md + +markdown_extensions: + - admonition + - mdx_include: + base_path: docs + - pymdownx.highlight + - pymdownx.superfences + - pymdownx.inlinehilite + - pymdownx.tabbed: + alternate_style: true diff --git a/.conflict-side-1/pyproject.toml b/.conflict-side-1/pyproject.toml new file mode 100644 index 0000000..9bcca87 --- /dev/null +++ b/.conflict-side-1/pyproject.toml @@ -0,0 +1,174 @@ +[project] +name = "grelmicro" +description = "Grelmicro is a lightweight framework/toolkit for building async microservices in Python" +license = "MIT" +authors = [{ name = "Loïc Gremaud", email = "grelinfo@gmail.com"}] +readme = "README.md" + +classifiers = [ + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python", + "Topic :: Internet", + "Topic :: Software Development :: Libraries :: Application Frameworks", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development", + "Typing :: Typed", + "Development Status :: 1 - Planning", + "Environment :: Web Environment", + "Framework :: AsyncIO", + "Framework :: FastAPI", + "Framework :: Pydantic", + "Framework :: Pydantic :: 2", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dynamic = ["version"] + +requires-python = ">=3.11" + +dependencies = [ + "anyio>=4.0.0", + "pydantic>=2.5.0", + "fast-depends>=2.0.0", + "pydantic-settings>=2.5.0", +] + +[project.urls] + +Repository = "https://github.com/grelinfo/grelmicro.git" +Issues = "https://github.com/grelinfo/grelmicro/issues" + +[project.optional-dependencies] +standard = [ + "loguru>=0.7.2", + "orjson>=3.10.11", +] +postgres = [ + "asyncpg>=0.30.0", +] +redis = [ + "redis>=5.0.0", +] + +[dependency-groups] +dev = [ + "pytest-cov>=6.0.0", + "pytest>=8.0.0", + "mypy>=1.12.0", + "ruff>=0.7.4", + "testcontainers[postgres,redis]>=4.8.2", + "pytest-timeout>=2.3.1", + "pytest-mock>=3.14.0", + "pytest-randomly>=3.16.0", + "pre-commit>=4.0.1", + "fastapi>=0.115.5", + "fastapi-cli>=0.0.5", + "mdx-include>=1.4.2", + "faststream>=0.5.30", + "hatch>=1.13.0", +] +docs = [ + "mkdocs-material>=9.5.44", + "pygments>=2.18.0", + "pymdown-extensions>=10.12", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build] +skip-excluded-dirs = true +exclude = ["/tests", "/docs", "/examples"] + +[tool.hatch.version] +path = "grelmicro/__init__.py" + +[tool.ruff] +target-version = "py311" +line-length = 80 + +[tool.ruff.lint] +select = ["ALL"] +ignore = ["COM812", "ISC001"] # Ignore rules conflicting with the formatter. + +[tool.ruff.lint.extend-per-file-ignores] +"examples/*" = [ + "ARG001", + "ANN001", + "ANN201", + "D103", + "D100", + "INP001", + "T201", +] +"examples/logging/basic.py" = ["EM101", "TRY"] +"examples/task/router.py" = ["I001", "E402"] +"tests/*" = [ + "S101", + "SLF001" +] + +[tool.ruff.lint.pycodestyle] +max-line-length = 100 # reports only line that exceed 100 characters. + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.pylint] +max-args = 10 + +[tool.mypy] +scripts_are_modules = true +plugins = [ + "pydantic.mypy" +] +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = true +check_untyped_defs = true +no_implicit_reexport = true +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = ["asyncpg", "testcontainers.*"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = [ + "examples.*" +] +disallow_untyped_defs = false + + +[tool.pytest.ini_options] +addopts = """ + --cov=grelmicro + --cov-report term:skip-covered + --cov-report xml:cov.xml + --strict-config + --strict-markers + -m "not integration" +""" +markers = """ + integration: mark a test as an integration test (disabled by default). +""" + +testpaths = "tests" + +[tool.coverage.report] +sort = "-Cover" +exclude_also = [ + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "assert_never\\(.*\\)", +] diff --git a/.conflict-side-1/tests/__init__.py b/.conflict-side-1/tests/__init__.py new file mode 100644 index 0000000..adc28b2 --- /dev/null +++ b/.conflict-side-1/tests/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Tests.""" diff --git a/.conflict-side-1/tests/conftest.py b/.conflict-side-1/tests/conftest.py new file mode 100644 index 0000000..916c148 --- /dev/null +++ b/.conflict-side-1/tests/conftest.py @@ -0,0 +1,9 @@ +"""Grelmicro Test Config.""" + +import pytest + + +@pytest.fixture +def anyio_backend() -> str: + """AnyIO Backend.""" + return "asyncio" diff --git a/.conflict-side-1/tests/logging/__init__.py b/.conflict-side-1/tests/logging/__init__.py new file mode 100644 index 0000000..a1c677a --- /dev/null +++ b/.conflict-side-1/tests/logging/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Logging Tests.""" diff --git a/.conflict-side-1/tests/logging/test_loguru.py b/.conflict-side-1/tests/logging/test_loguru.py new file mode 100644 index 0000000..9214250 --- /dev/null +++ b/.conflict-side-1/tests/logging/test_loguru.py @@ -0,0 +1,274 @@ +"""Test Logging Loguru.""" + +from collections.abc import Generator +from datetime import datetime +from io import StringIO + +import pytest +from loguru import logger +from pydantic import TypeAdapter + +from grelmicro.errors import DependencyNotFoundError +from grelmicro.logging.errors import LoggingSettingsValidationError +from grelmicro.logging.loguru import ( + JSON_FORMAT, + JSONRecordDict, + configure_logging, + json_formatter, + json_patcher, +) + +json_record_type_adapter = TypeAdapter(JSONRecordDict) + + +@pytest.fixture(autouse=True) +def cleanup_handlers() -> Generator[None, None, None]: + """Cleanup logging handlers.""" + logger.configure(handlers=[]) + yield + logger.remove() + + +def generate_logs() -> int: + """Generate logs.""" + logger.debug("Hello, World!") + logger.info("Hello, World!") + logger.warning("Hello, World!") + logger.error("Hello, Alice!", user="Alice") + try: + 1 / 0 # noqa: B018 + except ZeroDivisionError: + logger.exception("Hello, Bob!") + + return 5 + + +def assert_logs(logs: str) -> None: + """Assert logs.""" + ( + info, + warning, + error, + exception, + ) = ( + json_record_type_adapter.validate_json(line) + for line in logs.splitlines()[0:4] + ) + + expected_separator = 3 + + assert info["logger"] + assert info["logger"].startswith("tests.logging.test_loguru:generate_logs:") + assert len(info["logger"].split(":")) == expected_separator + assert info["time"] == datetime.fromisoformat(info["time"]).isoformat() + assert info["level"] == "INFO" + assert info["msg"] == "Hello, World!" + assert info["thread"] == "MainThread" + assert "ctx" not in info + + assert warning["logger"] + assert warning["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(warning["logger"].split(":")) == expected_separator + assert ( + warning["time"] == datetime.fromisoformat(warning["time"]).isoformat() + ) + assert warning["level"] == "WARNING" + assert warning["msg"] == "Hello, World!" + assert warning["thread"] == "MainThread" + assert "ctx" not in warning + + assert error["logger"] + assert error["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(error["logger"].split(":")) == expected_separator + assert error["time"] == datetime.fromisoformat(error["time"]).isoformat() + assert error["level"] == "ERROR" + assert error["msg"] == "Hello, Alice!" + assert error["thread"] == "MainThread" + assert error["ctx"] == {"user": "Alice"} + + assert exception["logger"] + assert exception["logger"].startswith( + "tests.logging.test_loguru:generate_logs:" + ) + assert len(exception["logger"].split(":")) == expected_separator + assert ( + exception["time"] + == datetime.fromisoformat(exception["time"]).isoformat() + ) + assert exception["level"] == "ERROR" + assert exception["msg"] == "Hello, Bob!" + assert exception["thread"] == "MainThread" + assert exception["ctx"] == { + "exception": "ZeroDivisionError: division by zero", + } + + +def test_json_formatter() -> None: + """Test JSON Formatter.""" + # Arrange + sink = StringIO() + + # Act + logger.add(sink, format=json_formatter, level="INFO") + generate_logs() + + # Assert + assert_logs(sink.getvalue()) + + +def test_json_patching() -> None: + """Test JSON Patching.""" + # Arrange + sink = StringIO() + + # Act + # logger.patch(json_patcher) -> Patch is not working using logger.configure instead + logger.configure(patcher=json_patcher) + logger.add(sink, format=JSON_FORMAT, level="INFO") + generate_logs() + + # Assert + assert_logs(sink.getvalue()) + + +def test_configure_logging_default( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Default.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + configure_logging() + generate_logs() + + # Assert + assert_logs(capsys.readouterr().out) + + +def test_configure_logging_text( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Text.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "text") + + # Act + configure_logging() + generate_logs() + + # Assert + lines = capsys.readouterr().out.splitlines() + + assert "tests.logging.test_loguru:generate_logs:" in lines[0] + assert " | INFO | " in lines[0] + assert " - Hello, World!" in lines[0] + + assert "tests.logging.test_loguru:generate_logs:" in lines[1] + assert " | WARNING | " in lines[1] + assert " - Hello, World!" in lines[1] + + assert "tests.logging.test_loguru:generate_logs:" in lines[2] + assert " | ERROR | " in lines[2] + assert " - Hello, Alice!" in lines[2] + + assert "tests.logging.test_loguru:generate_logs:" in lines[3] + assert " | ERROR | " in lines[3] + assert " - Hello, Bob!" in lines[3] + assert "Traceback" in lines[4] + assert "ZeroDivisionError: division by zero" in lines[-1] + + +def test_configure_logging_json( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging JSON.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "json") + + # Act + configure_logging() + generate_logs() + + # Assert + assert_logs(capsys.readouterr().out) + + +def test_configure_logging_level( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Level.""" + # Arrange + monkeypatch.setenv("LOG_LEVEL", "DEBUG") + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + configure_logging() + logs_count = generate_logs() + + # Assert + assert len(capsys.readouterr().out.splitlines()) == logs_count + + +def test_configure_logging_invalid_level( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Invalid Level.""" + # Arrange + monkeypatch.setenv("LOG_LEVEL", "INVALID") + monkeypatch.delenv("LOG_FORMAT", raising=False) + + # Act + with pytest.raises( + LoggingSettingsValidationError, + match=( + r"Could not validate environment variables settings:\n" + r"- LOG_LEVEL: Input should be 'DEBUG', 'INFO', 'WARNING', 'ERROR' or 'CRITICAL'" + r" \[input=INVALID\]" + ), + ): + configure_logging() + + # Assert + assert not capsys.readouterr().out + + +def test_configure_logging_format_template( + capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Configure Logging Format Template.""" + # Arrange + monkeypatch.delenv("LOG_LEVEL", raising=False) + monkeypatch.setenv("LOG_FORMAT", "{level}: {message}") + + # Act + configure_logging() + generate_logs() + + # Assert + lines = capsys.readouterr().out.splitlines() + assert "INFO: Hello, World!" in lines[0] + assert "WARNING: Hello, World!" in lines[1] + assert "ERROR: Hello, Alice!" in lines[2] + assert "ERROR: Hello, Bob!" in lines[3] + assert "Traceback" in lines[4] + assert "ZeroDivisionError: division by zero" in lines[-1] + + +def test_configure_logging_dependency_not_found( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Configure Logging Dependency Not Found.""" + # Arrange + monkeypatch.setattr("grelmicro.logging.loguru.loguru", None) + + # Act / Assert + with pytest.raises(DependencyNotFoundError, match="loguru"): + configure_logging() diff --git a/.conflict-side-1/tests/sync/__init__.py b/.conflict-side-1/tests/sync/__init__.py new file mode 100644 index 0000000..5e3b5c4 --- /dev/null +++ b/.conflict-side-1/tests/sync/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Synchronization Primitives Tests.""" diff --git a/.conflict-side-1/tests/sync/test_backends.py b/.conflict-side-1/tests/sync/test_backends.py new file mode 100644 index 0000000..b08a92f --- /dev/null +++ b/.conflict-side-1/tests/sync/test_backends.py @@ -0,0 +1,370 @@ +"""Test Synchronization Backends.""" + +from collections.abc import AsyncGenerator, Callable, Generator +from uuid import uuid4 + +import pytest +from anyio import sleep +from testcontainers.core.container import DockerContainer +from testcontainers.postgres import PostgresContainer +from testcontainers.redis import RedisContainer + +from grelmicro.sync._backends import get_sync_backend, loaded_backends +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import BackendNotLoadedError +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.sync.postgres import PostgresSyncBackend +from grelmicro.sync.redis import RedisSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(15)] + + +@pytest.fixture(scope="module") +def anyio_backend() -> str: + """AnyIO Backend Module Scope.""" + return "asyncio" + + +@pytest.fixture(scope="module") +def monkeypatch() -> Generator[pytest.MonkeyPatch, None, None]: + """Monkeypatch Module Scope.""" + monkeypatch = pytest.MonkeyPatch() + yield monkeypatch + monkeypatch.undo() + + +@pytest.fixture +def clean_registry() -> Generator[None, None, None]: + """Make sure the registry is clean.""" + loaded_backends.pop("lock", None) + yield + loaded_backends.pop("lock", None) + + +@pytest.fixture( + params=[ + "memory", + pytest.param("redis", marks=[pytest.mark.integration]), + pytest.param("postgres", marks=[pytest.mark.integration]), + ], + scope="module", +) +def backend_name(request: pytest.FixtureRequest) -> str: + """Backend Name.""" + return request.param + + +@pytest.fixture( + scope="module", +) +def container( + backend_name: str, + monkeypatch: pytest.MonkeyPatch, +) -> Generator[DockerContainer | None, None, None]: + """Test Container for each Backend.""" + if backend_name == "redis": + with RedisContainer() as container: + yield container + elif backend_name == "postgres": + monkeypatch.setenv("POSTGRES_HOST", "localhost") + monkeypatch.setenv("POSTGRES_PORT", "5432") + monkeypatch.setenv("POSTGRES_DB", "test") + monkeypatch.setenv("POSTGRES_USER", "test") + monkeypatch.setenv("POSTGRES_PASSWORD", "test") + with PostgresContainer() as container: + yield container + elif backend_name == "memory": + yield None + + +@pytest.fixture(scope="module") +async def backend( + backend_name: str, container: DockerContainer | None +) -> AsyncGenerator[SyncBackend]: + """Test Container for each Backend.""" + if backend_name == "redis" and container: + port = container.get_exposed_port(6379) + async with RedisSyncBackend(f"redis://localhost:{port}/0") as backend: + yield backend + elif backend_name == "postgres" and container: + port = container.get_exposed_port(5432) + async with PostgresSyncBackend( + f"postgresql://test:test@localhost:{port}/test" + ) as backend: + yield backend + elif backend_name == "memory": + async with MemorySyncBackend() as backend: + yield backend + + +async def test_acquire(backend: SyncBackend) -> None: + """Test acquire.""" + # Arrange + name = "test_acquire" + token = uuid4().hex + duration = 1 + + # Act + result = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result + + +async def test_acquire_reantrant(backend: SyncBackend) -> None: + """Test acquire is reantrant.""" + # Arrange + name = "test_acquire_reantrant" + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result1 + assert result2 + + +async def test_acquire_already_acquired(backend: SyncBackend) -> None: + """Test acquire when already acquired.""" + # Arrange + name = "test_acquire_already_acquired" + token1 = uuid4().hex + token2 = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token1, duration=duration) + result2 = await backend.acquire(name=name, token=token2, duration=duration) + + # Assert + assert token1 != token2 + assert result1 + assert not result2 + + +async def test_acquire_expired(backend: SyncBackend) -> None: + """Test acquire when expired.""" + # Arrange + name = "test_acquire_expired" + token = uuid4().hex + duration = 0.01 + + # Act + result = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.acquire(name=name, token=token, duration=duration) + + # Assert + assert result + assert result2 + + +async def test_acquire_already_acquired_expired(backend: SyncBackend) -> None: + """Test acquire when already acquired but expired.""" + # Arrange + name = "test_acquire_already_acquired_expired" + uuid4().hex + token1 = uuid4().hex + token2 = uuid4().hex + duration = 0.01 + + # Act + result = await backend.acquire(name=name, token=token1, duration=duration) + await sleep(duration * 2) + result2 = await backend.acquire(name=name, token=token2, duration=duration) + + # Assert + assert token1 != token2 + assert result + assert result2 + + +async def test_release_not_acquired(backend: SyncBackend) -> None: + """Test release when not acquired.""" + # Arrange + name = "test_release" + uuid4().hex + token = uuid4().hex + + # Act + result = await backend.release(name=name, token=token) + + # Assert + assert not result + + +async def test_release_acquired(backend: SyncBackend) -> None: + """Test release when acquired.""" + # Arrange + name = "test_release_acquired" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert result2 + + +async def test_release_not_reantrant(backend: SyncBackend) -> None: + """Test release is not reantrant.""" + # Arrange + name = "test_release_not_reantrant" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + result2 = await backend.release(name=name, token=token) + result3 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert result2 + assert not result3 + + +async def test_release_acquired_expired(backend: SyncBackend) -> None: + """Test release when acquired but expired.""" + # Arrange + name = "test_release_acquired_expired" + uuid4().hex + token = uuid4().hex + duration = 0.01 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert not result2 + + +async def test_release_not_acquired_expired(backend: SyncBackend) -> None: + """Test release when not acquired but expired.""" + # Arrange + name = "test_release_not_acquired_expired" + uuid4().hex + token = uuid4().hex + duration = 0.01 + + # Act + result1 = await backend.acquire(name=name, token=token, duration=duration) + await sleep(duration * 2) + result2 = await backend.release(name=name, token=token) + + # Assert + assert result1 + assert not result2 + + +async def test_locked(backend: SyncBackend) -> None: + """Test locked.""" + # Arrange + name = "test_locked" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + locked_before = await backend.locked(name=name) + await backend.acquire(name=name, token=token, duration=duration) + locked_after = await backend.locked(name=name) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_owned(backend: SyncBackend) -> None: + """Test owned.""" + # Arrange + name = "test_owned" + uuid4().hex + token = uuid4().hex + duration = 1 + + # Act + owned_before = await backend.owned(name=name, token=token) + await backend.acquire(name=name, token=token, duration=duration) + owned_after = await backend.owned(name=name, token=token) + + # Assert + assert owned_before is False + assert owned_after is True + + +async def test_owned_another(backend: SyncBackend) -> None: + """Test owned another.""" + # Arrange + name = "test_owned_another" + uuid4().hex + token1 = uuid4().hex + token2 = uuid4().hex + duration = 1 + + # Act + owned_before = await backend.owned(name=name, token=token1) + await backend.acquire(name=name, token=token1, duration=duration) + owned_after = await backend.owned(name=name, token=token2) + + # Assert + assert owned_before is False + assert owned_after is False + + +@pytest.mark.parametrize( + "backend_factory", + [ + lambda: MemorySyncBackend(), + lambda: RedisSyncBackend("redis://localhost:6379/0"), + lambda: PostgresSyncBackend( + "postgresql://user:password@localhost:5432/db" + ), + ], +) +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend(backend_factory: Callable[[], SyncBackend]) -> None: + """Test Get Synchronization Backend.""" + # Arrange + expected_backend = backend_factory() + + # Act + backend = get_sync_backend() + + # Assert + assert backend is expected_backend + + +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend_not_loaded() -> None: + """Test Get Synchronization Backend Not Loaded.""" + # Act / Assert + with pytest.raises(BackendNotLoadedError): + get_sync_backend() + + +@pytest.mark.parametrize( + "backend_factory", + [ + lambda: MemorySyncBackend(auto_register=False), + lambda: RedisSyncBackend( + "redis://localhost:6379/0", auto_register=False + ), + lambda: PostgresSyncBackend( + "postgresql://user:password@localhost:5432/db", auto_register=False + ), + ], +) +@pytest.mark.usefixtures("clean_registry") +def test_get_sync_backend_auto_register_disabled( + backend_factory: Callable[[], SyncBackend], +) -> None: + """Test Get Synchronization Backend.""" + # Arrange + backend_factory() + + # Act / Assert + with pytest.raises(BackendNotLoadedError): + get_sync_backend() diff --git a/.conflict-side-1/tests/sync/test_leaderelection.py b/.conflict-side-1/tests/sync/test_leaderelection.py new file mode 100644 index 0000000..d357daa --- /dev/null +++ b/.conflict-side-1/tests/sync/test_leaderelection.py @@ -0,0 +1,457 @@ +"""Test leader election.""" + +import math + +import pytest +from anyio import Event, create_task_group, sleep +from pydantic import ValidationError +from pytest_mock import MockerFixture + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.leaderelection import LeaderElection, LeaderElectionConfig +from grelmicro.sync.memory import MemorySyncBackend + +WORKERS = 4 +WORKER_1 = 0 +WORKER_2 = 1 +TEST_TIMEOUT = 1 + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(TEST_TIMEOUT)] + + +@pytest.fixture +def backend() -> SyncBackend: + """Return Memory Synchronization Backend.""" + return MemorySyncBackend() + + +@pytest.fixture +def configs() -> list[LeaderElectionConfig]: + """Leader election Config.""" + return [ + LeaderElectionConfig( + name="test_leader_election", + worker=f"worker_{i}", + lease_duration=0.02, + renew_deadline=0.015, + retry_interval=0.005, + error_interval=0.01, + backend_timeout=0.005, + ) + for i in range(WORKERS) + ] + + +@pytest.fixture +def leader_elections( + backend: SyncBackend, configs: list[LeaderElectionConfig] +) -> list[LeaderElection]: + """Leader elections.""" + return [ + LeaderElection(backend=backend, **configs[i].model_dump()) + for i in range(WORKERS) + ] + + +@pytest.fixture +def leader_election( + backend: SyncBackend, configs: list[LeaderElectionConfig] +) -> LeaderElection: + """Leader election.""" + return LeaderElection(backend=backend, **configs[WORKER_1].model_dump()) + + +async def wait_first_leader(leader_elections: list[LeaderElection]) -> None: + """Wait for the first leader to be elected.""" + + async def wrapper(leader_election: LeaderElection, event: Event) -> None: + """Wait for the leadership.""" + await leader_election.wait_for_leader() + event.set() + + async with create_task_group() as task_group: + event = Event() + for coroutine in leader_elections: + task_group.start_soon(wrapper, coroutine, event) + await event.wait() + task_group.cancel_scope.cancel() + + +def test_leader_election_config() -> None: + """Test leader election Config.""" + # Arrange + config = LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + lease_duration=0.01, + renew_deadline=0.008, + retry_interval=0.001, + error_interval=0.01, + backend_timeout=0.007, + ) + + # Assert + assert config.model_dump() == { + "name": "test_leader_election", + "worker": "worker_1", + "lease_duration": 0.01, + "renew_deadline": 0.008, + "retry_interval": 0.001, + "error_interval": 0.01, + "backend_timeout": 0.007, + } + + +def test_leader_election_config_defaults() -> None: + """Test leader election Config Defaults.""" + # Arrange + config = LeaderElectionConfig( + name="test_leader_election", worker="worker_1" + ) + + # Assert + assert config.model_dump() == { + "name": "test_leader_election", + "worker": "worker_1", + "lease_duration": 15, + "renew_deadline": 10, + "retry_interval": 2, + "error_interval": 30, + "backend_timeout": 5, + } + + +def test_leader_election_config_validation_errors() -> None: + """Test leader election Config Errors.""" + # Arrange + with pytest.raises( + ValidationError, + match="Renew deadline must be shorter than lease duration", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + lease_duration=15, + renew_deadline=20, + ) + with pytest.raises( + ValidationError, + match="Retry interval must be shorter than renew deadline", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + renew_deadline=10, + retry_interval=15, + ) + with pytest.raises( + ValidationError, + match="Backend timeout must be shorter than renew deadline", + ): + LeaderElectionConfig( + name="test_leader_election", + worker="worker_1", + renew_deadline=10, + backend_timeout=15, + ) + + +async def test_lifecycle(leader_election: LeaderElection) -> None: + """Test leader election on worker complete lifecycle.""" + # Act + is_leader_before_start = leader_election.is_leader() + is_running_before_start = leader_election.is_running() + async with create_task_group() as tg: + await tg.start(leader_election) + is_running_after_start = leader_election.is_running() + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + tg.cancel_scope.cancel() + is_running_after_cancel = leader_election.is_running() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_cancel is False + + assert is_running_before_start is False + assert is_running_after_start is True + assert is_running_after_cancel is False + + +async def test_leader_election_context_manager( + leader_election: LeaderElection, +) -> None: + """Test leader election on worker using context manager.""" + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + async with leader_election: + is_leader_inside_context = leader_election.is_leader() + is_leader_after_context = leader_election.is_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_inside_context is True + assert is_leader_after_context is True + assert is_leader_after_cancel is False + + +async def test_leader_election_single_worker( + leader_election: LeaderElection, +) -> None: + """Test leader election on single worker.""" + # Act + async with create_task_group() as tg: + is_leader_before_start = leader_election.is_leader() + await tg.start(leader_election) + is_leader_inside_context = leader_election.is_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + is_leader_after_cancel = leader_election.is_leader() + + # Assert + assert is_leader_before_start is False + assert is_leader_inside_context is True + assert is_leader_after_cancel is False + + +async def test_leadership_abandon_on_renew_deadline_reached( + leader_election: LeaderElection, +) -> None: + """Test leader election abandons leadership when renew deadline is reached.""" + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + leader_election.config.retry_interval = math.inf + await leader_election.wait_lose_leader() + is_leader_after_not_renewed = leader_election.is_leader() + tg.cancel_scope.cancel() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_not_renewed is False + + +async def test_leadership_abandon_on_backend_failure( + leader_election: LeaderElection, + caplog: pytest.LogCaptureFixture, + mocker: MockerFixture, +) -> None: + """Test leader election abandons leadership when backend is unreachable.""" + # Arrange + caplog.set_level("WARNING") + + # Act + is_leader_before_start = leader_election.is_leader() + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + is_leader_after_start = leader_election.is_leader() + mocker.patch.object( + leader_election.backend, + "acquire", + side_effect=Exception("Backend Unreachable"), + ) + await leader_election.wait_lose_leader() + is_leader_after_not_renewed = leader_election.is_leader() + tg.cancel_scope.cancel() + + # Assert + assert is_leader_before_start is False + assert is_leader_after_start is True + assert is_leader_after_not_renewed is False + assert ( + "Leader Election lost leadership: test_leader_election (renew deadline reached)" + in caplog.messages + ) + + +async def test_unepexpected_stop( + leader_election: LeaderElection, mocker: MockerFixture +) -> None: + """Test leader election worker abandons leadership on unexpected stop.""" + + # Arrange + async def leader_election_unexpected_exception() -> None: + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + mock = mocker.patch.object( + leader_election, + "_try_acquire_or_renew", + side_effect=Exception("Unexpected Exception"), + ) + await leader_election.wait_lose_leader() + mock.reset_mock() + tg.cancel_scope.cancel() + + # Act / Assert + with pytest.raises(ExceptionGroup): + await leader_election_unexpected_exception() + + +async def test_release_on_cancel( + backend: SyncBackend, leader_election: LeaderElection, mocker: MockerFixture +) -> None: + """Test leader election on worker that releases the lock on cancel.""" + # Arrange + spy_release = mocker.spy(backend, "release") + + # Act + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + + # Assert + spy_release.assert_called_once() + + +async def test_release_failure_ignored( + backend: SyncBackend, + leader_election: LeaderElection, + mocker: MockerFixture, +) -> None: + """Test leader election on worker that ignores release failure.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Unreachable") + ) + + # Act + async with create_task_group() as tg: + await tg.start(leader_election) + await leader_election.wait_for_leader() + tg.cancel_scope.cancel() + await leader_election.wait_lose_leader() + + +async def test_only_one_leader(leader_elections: list[LeaderElection]) -> None: + """Test leader election on multiple workers ensuring only one leader is elected.""" + # Act + leaders_before_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + async with create_task_group() as tg: + for leader_election in leader_elections: + await tg.start(leader_election) + await wait_first_leader(leader_elections) + leaders_after_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + tg.cancel_scope.cancel() + for leader_election in leader_elections: + await leader_election.wait_lose_leader() + leaders_after_cancel = [ + leader_election.is_leader() for leader_election in leader_elections + ] + + # Assert + assert sum(leaders_before_start) == 0 + assert sum(leaders_after_start) == 1 + assert sum(leaders_after_cancel) == 0 + + +async def test_leader_transition( + leader_elections: list[LeaderElection], +) -> None: + """Test leader election leader transition to another worker.""" + # Arrange + leaders_after_leader_election1_start = [False] * len(leader_elections) + leaders_after_all_start = [False] * len(leader_elections) + leaders_after_leader_election1_down = [False] * len(leader_elections) + + # Act + leaders_before_start = [ + leader_election.is_leader() for leader_election in leader_elections + ] + async with create_task_group() as workers_tg: + async with create_task_group() as worker1_tg: + await worker1_tg.start(leader_elections[WORKER_1]) + await leader_elections[WORKER_1].wait_for_leader() + leaders_after_leader_election1_start = [ + leader_election.is_leader() + for leader_election in leader_elections + ] + + for leader_election in leader_elections: + await workers_tg.start(leader_election) + leaders_after_all_start = [ + leader_election.is_leader() + for leader_election in leader_elections + ] + worker1_tg.cancel_scope.cancel() + + await leader_elections[WORKER_1].wait_lose_leader() + + await wait_first_leader(leader_elections) + leaders_after_leader_election1_down = [ + leader_election.is_leader() for leader_election in leader_elections + ] + workers_tg.cancel_scope.cancel() + + for leader_election in leader_elections[WORKER_2:]: + await leader_election.wait_lose_leader() + leaders_after_all_down = [ + leader_election.is_leader() for leader_election in leader_elections + ] + + # Assert + assert sum(leaders_before_start) == 0 + assert sum(leaders_after_leader_election1_start) == 1 + assert sum(leaders_after_all_start) == 1 + assert sum(leaders_after_leader_election1_down) == 1 + assert sum(leaders_after_all_down) == 0 + + assert leaders_after_leader_election1_start[WORKER_1] is True + assert leaders_after_leader_election1_down[WORKER_1] is False + + +async def test_error_interval( + backend: SyncBackend, + leader_elections: list[LeaderElection], + caplog: pytest.LogCaptureFixture, + mocker: MockerFixture, +) -> None: + """Test leader election on worker with error cooldown.""" + # Arrange + caplog.set_level("ERROR") + leader_elections[WORKER_1].config.error_interval = 1 + leader_elections[WORKER_2].config.error_interval = 0.001 + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Unreachable") + ) + + # Act + async with create_task_group() as tg: + await tg.start(leader_elections[WORKER_1]) + await sleep(0.01) + tg.cancel_scope.cancel() + leader_election1_nb_errors = sum( + 1 for record in caplog.records if record.levelname == "ERROR" + ) + caplog.clear() + + async with create_task_group() as tg: + await tg.start(leader_elections[WORKER_2]) + await sleep(0.01) + tg.cancel_scope.cancel() + leader_election2_nb_errors = sum( + 1 for record in caplog.records if record.levelname == "ERROR" + ) + + # Assert + assert leader_election1_nb_errors == 1 + assert leader_election2_nb_errors >= 1 diff --git a/.conflict-side-1/tests/sync/test_lock.py b/.conflict-side-1/tests/sync/test_lock.py new file mode 100644 index 0000000..42e0b04 --- /dev/null +++ b/.conflict-side-1/tests/sync/test_lock.py @@ -0,0 +1,506 @@ +"""Test Lock.""" + +import time +from collections.abc import AsyncGenerator + +import pytest +from anyio import WouldBlock, sleep, to_thread +from pytest_mock import MockerFixture + +from grelmicro.sync.abc import SyncBackend +from grelmicro.sync.errors import ( + LockAcquireError, + LockNotOwnedError, + LockReleaseError, + SyncBackendError, +) +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +WORKER_1 = 0 +WORKER_2 = 1 +WORKER_COUNT = 2 + +LOCK_NAME = "test_leased_lock" + + +@pytest.fixture +async def backend() -> AsyncGenerator[SyncBackend]: + """Return Memory Synchronization Backend.""" + async with MemorySyncBackend() as backend: + yield backend + + +@pytest.fixture +def locks(backend: SyncBackend) -> list[Lock]: + """Locks of multiple workers.""" + return [ + Lock( + backend=backend, + name=LOCK_NAME, + worker=f"worker_{i}", + lease_duration=0.01, + retry_interval=0.001, + ) + for i in range(WORKER_COUNT) + ] + + +@pytest.fixture +def lock(locks: list[Lock]) -> Lock: + """Lock.""" + return locks[WORKER_1] + + +async def test_lock_owned(locks: list[Lock]) -> None: + """Test Lock owned.""" + # Act + worker_1_owned_before = await locks[WORKER_1].owned() + worker_2_owned_before = await locks[WORKER_2].owned() + await locks[WORKER_1].acquire() + worker_1_owned_after = await locks[WORKER_1].owned() + worker_2_owned_after = await locks[WORKER_2].owned() + + # Assert + assert worker_1_owned_before is False + assert worker_2_owned_before is False + assert worker_1_owned_after is True + assert worker_2_owned_after is False + + +async def test_lock_from_thread_owned(locks: list[Lock]) -> None: + """Test Lock from thread owned.""" + # Arrange + worker_1_owned_before = None + worker_2_owned_before = None + worker_1_owned_after = None + worker_2_owned_after = None + + # Act + def sync() -> None: + nonlocal worker_1_owned_before + nonlocal worker_2_owned_before + nonlocal worker_1_owned_after + nonlocal worker_2_owned_after + + worker_1_owned_before = locks[WORKER_1].from_thread.owned() + worker_2_owned_before = locks[WORKER_2].from_thread.owned() + locks[WORKER_1].from_thread.acquire() + worker_1_owned_after = locks[WORKER_1].from_thread.owned() + worker_2_owned_after = locks[WORKER_2].from_thread.owned() + + await to_thread.run_sync(sync) + + # Assert + assert worker_1_owned_before is False + assert worker_2_owned_before is False + assert worker_1_owned_after is True + assert worker_2_owned_after is False + + +async def test_lock_context_manager(lock: Lock) -> None: + """Test Lock context manager.""" + # Act + locked_before = await lock.locked() + async with lock: + locked_inside = await lock.locked() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_inside is True + assert locked_after is False + + +async def test_lock_from_thread_context_manager_acquire(lock: Lock) -> None: + """Test Lock from thread context manager.""" + # Arrange + locked_before = None + locked_inside = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_inside + nonlocal locked_after + + locked_before = lock.from_thread.locked() + with lock.from_thread: + locked_inside = lock.from_thread.locked() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_inside is True + assert locked_after is False + + +async def test_lock_context_manager_wait(lock: Lock, locks: list[Lock]) -> None: + """Test Lock context manager wait.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act + locked_before = await lock.locked() + async with locks[WORKER_2]: # Wait until lock expires + locked_inside = await lock.locked() + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_inside is True + assert locked_after is False + + +async def test_lock_from_thread_context_manager_wait( + lock: Lock, locks: list[Lock] +) -> None: + """Test Lock from thread context manager wait.""" + # Arrange + locked_before = None + locked_inside = None + locked_after = None + await locks[WORKER_1].acquire() + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_inside + nonlocal locked_after + + locked_before = lock.from_thread.locked() + with locks[WORKER_2].from_thread: + locked_inside = lock.from_thread.locked() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is True + assert locked_inside is True + assert locked_after is False + + +async def test_lock_acquire(lock: Lock) -> None: + """Test Lock acquire.""" + # Act + locked_before = await lock.locked() + await lock.acquire() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_from_thread_acquire(lock: Lock) -> None: + """Test Lock from thread acquire.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_wait(lock: Lock, locks: list[Lock]) -> None: + """Test Lock acquire wait.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act + locked_before = await lock.locked() + await locks[WORKER_2].acquire() # Wait until lock expires + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_after is True + + +async def test_lock_from_thread_acquire_wait(lock: Lock) -> None: + """Test Lock from thread acquire wait.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_nowait(lock: Lock) -> None: + """Test Lock wait acquire.""" + # Act + locked_before = await lock.locked() + await lock.acquire_nowait() + locked_after = await lock.locked() + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_from_thread_acquire_nowait(lock: Lock) -> None: + """Test Lock from thread wait acquire.""" + # Arrange + locked_before = None + locked_after = None + + # Act + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + locked_before = lock.from_thread.locked() + lock.from_thread.acquire_nowait() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is False + assert locked_after is True + + +async def test_lock_acquire_nowait_would_block(locks: list[Lock]) -> None: + """Test Lock wait acquire would block.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act / Assert + with pytest.raises(WouldBlock): + await locks[WORKER_2].acquire_nowait() + + +async def test_lock_from_thread_acquire_nowait_would_block( + locks: list[Lock], +) -> None: + """Test Lock from thread wait acquire would block.""" + # Arrange + await locks[WORKER_1].acquire() + + # Act / Assert + def sync() -> None: + with pytest.raises(WouldBlock): + locks[WORKER_2].from_thread.acquire_nowait() + + await to_thread.run_sync(sync) + + +async def test_lock_release(lock: Lock) -> None: + """Test Lock release.""" + # Act / Assert + with pytest.raises(LockNotOwnedError): + await lock.release() + + +async def test_lock_from_thread_release(lock: Lock) -> None: + """Test Lock from thread release.""" + + # Act / Assert + def sync() -> None: + with pytest.raises(LockNotOwnedError): + lock.from_thread.release() + + await to_thread.run_sync(sync) + + +async def test_lock_release_acquired(lock: Lock) -> None: + """Test Lock release acquired.""" + # Arrange + await lock.acquire() + + # Act + locked_before = await lock.locked() + await lock.release() + locked_after = await lock.locked() + + # Assert + assert locked_before is True + assert locked_after is False + + +async def test_lock_from_thread_release_acquired(lock: Lock) -> None: + """Test Lock from thread release acquired.""" + # Arrange + locked_before = None + locked_after = None + + def sync() -> None: + nonlocal locked_before + nonlocal locked_after + + lock.from_thread.acquire() + + # Act + locked_before = lock.from_thread.locked() + lock.from_thread.release() + locked_after = lock.from_thread.locked() + + await to_thread.run_sync(sync) + + # Assert + assert locked_before is True + assert locked_after is False + + +async def test_lock_release_expired(locks: list[Lock]) -> None: + """Test Lock release expired.""" + # Arrange + await locks[WORKER_1].acquire() + await sleep(locks[WORKER_1].config.lease_duration) + + # Act + worker_1_locked_before = await locks[WORKER_1].locked() + with pytest.raises(LockNotOwnedError): + await locks[WORKER_2].release() + + # Assert + assert worker_1_locked_before is False + + +async def test_lock_from_thread_release_expired(locks: list[Lock]) -> None: + """Test Lock from thread release expired.""" + # Arrange + worker_1_locked_before = None + + def sync() -> None: + nonlocal worker_1_locked_before + + locks[WORKER_1].from_thread.acquire() + time.sleep(locks[WORKER_1].config.lease_duration) + + # Act + worker_1_locked_before = locks[WORKER_1].from_thread.locked() + with pytest.raises(LockNotOwnedError): + locks[WORKER_2].from_thread.release() + + await to_thread.run_sync(sync) + + # Assert + assert worker_1_locked_before is False + + +async def test_lock_acquire_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock acquire backend error.""" + # Arrange + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Error") + ) + + # Act + with pytest.raises(LockAcquireError): + await lock.acquire() + + +async def test_lock_from_thread_acquire_backend_error( + backend: SyncBackend, + lock: Lock, + mocker: MockerFixture, +) -> None: + """Test Lock from thread acquire backend error.""" + # Arrange + mocker.patch.object( + backend, "acquire", side_effect=Exception("Backend Error") + ) + + # Act + def sync() -> None: + with pytest.raises(LockAcquireError): + lock.from_thread.acquire() + + await to_thread.run_sync(sync) + + +async def test_lock_release_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock release backend error.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Error") + ) + + # Act + await lock.acquire() + with pytest.raises(LockReleaseError): + await lock.release() + + +async def test_lock_from_thread_release_backend_error( + backend: SyncBackend, + lock: Lock, + mocker: MockerFixture, +) -> None: + """Test Lock from thread release backend error.""" + # Arrange + mocker.patch.object( + backend, "release", side_effect=Exception("Backend Error") + ) + + # Act + def sync() -> None: + lock.from_thread.acquire() + with pytest.raises(LockReleaseError): + lock.from_thread.release() + + await to_thread.run_sync(sync) + + +async def test_lock_owned_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock owned backend error.""" + # Arrange + mocker.patch.object( + backend, "owned", side_effect=Exception("Backend Error") + ) + + # Act / Assert + with pytest.raises(SyncBackendError): + await lock.owned() + + +async def test_lock_locked_backend_error( + backend: SyncBackend, lock: Lock, mocker: MockerFixture +) -> None: + """Test Lock locked backend error.""" + # Arrange + mocker.patch.object( + backend, "locked", side_effect=Exception("Backend Error") + ) + + # Act / Assert + with pytest.raises(SyncBackendError): + await lock.locked() diff --git a/.conflict-side-1/tests/sync/test_postgres.py b/.conflict-side-1/tests/sync/test_postgres.py new file mode 100644 index 0000000..ef8dd18 --- /dev/null +++ b/.conflict-side-1/tests/sync/test_postgres.py @@ -0,0 +1,106 @@ +"""Tests for PostgreSQL Backends.""" + +import pytest + +from grelmicro.errors import OutOfContextError +from grelmicro.sync.errors import SyncSettingsValidationError +from grelmicro.sync.postgres import PostgresSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +URL = "postgres://user:password@localhost:5432/db" + + +@pytest.mark.parametrize( + "table_name", + [ + "locks table", + "%locks", + "locks;table", + "locks' OR '1'='1", + "locks; DROP TABLE users; --", + ], +) +def test_sync_backend_table_name_invalid(table_name: str) -> None: + """Test Synchronization Backend Table Name Invalid.""" + # Act / Assert + with pytest.raises( + ValueError, match="Table name '.*' is not a valid identifier" + ): + PostgresSyncBackend(url=URL, table_name=table_name) + + +async def test_sync_backend_out_of_context_errors() -> None: + """Test Synchronization Backend Out Of Context Errors.""" + # Arrange + backend = PostgresSyncBackend(url=URL) + name = "lock" + key = "token" + + # Act / Assert + with pytest.raises(OutOfContextError): + await backend.acquire(name=name, token=key, duration=1) + with pytest.raises(OutOfContextError): + await backend.release(name=name, token=key) + with pytest.raises(OutOfContextError): + await backend.locked(name=name) + with pytest.raises(OutOfContextError): + await backend.owned(name=name, token=key) + + +@pytest.mark.parametrize( + ("environs"), + [ + { + "POSTGRES_URL": "postgresql://test_user:test_password@test_host:1234/test_db" + }, + { + "POSTGRES_USER": "test_user", + "POSTGRES_PASSWORD": "test_password", + "POSTGRES_HOST": "test_host", + "POSTGRES_PORT": "1234", + "POSTGRES_DB": "test_db", + }, + ], +) +def test_postgres_env_var_settings( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test PostgreSQL Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Act + backend = PostgresSyncBackend() + + # Assert + assert ( + backend._url + == "postgresql://test_user:test_password@test_host:1234/test_db" + ) + + +@pytest.mark.parametrize( + ("environs"), + [ + { + "POSTGRES_URL": "test://test_user:test_password@test_host:1234/test_db" + }, + {"POSTGRES_USER": "test_user"}, + ], +) +def test_postgres_env_var_settings_validation_error( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test PostgreSQL Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Assert / Act + with pytest.raises( + SyncSettingsValidationError, + match=(r"Could not validate environment variables settings:\n"), + ): + PostgresSyncBackend() diff --git a/.conflict-side-1/tests/sync/test_redis.py b/.conflict-side-1/tests/sync/test_redis.py new file mode 100644 index 0000000..a14bad7 --- /dev/null +++ b/.conflict-side-1/tests/sync/test_redis.py @@ -0,0 +1,67 @@ +"""Tests for Redis Backends.""" + +import pytest + +from grelmicro.sync.errors import SyncSettingsValidationError +from grelmicro.sync.redis import RedisSyncBackend + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] + +URL = "redis://:test_password@test_host:1234/0" + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": URL}, + { + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Act + backend = RedisSyncBackend() + + # Assert + assert backend._url == URL + + +@pytest.mark.parametrize( + ("environs"), + [ + {"REDIS_URL": "test://:test_password@test_host:1234/0"}, + {"REDIS_PASSWORD": "test_password"}, + { + "REDIS_URL": "test://:test_password@test_host:1234/0", + "REDIS_PASSWORD": "test_password", + "REDIS_HOST": "test_host", + "REDIS_PORT": "1234", + "REDIS_DB": "0", + }, + ], +) +def test_redis_env_var_settings_validation_error( + environs: dict[str, str], monkeypatch: pytest.MonkeyPatch +) -> None: + """Test Redis Settings from Environment Variables.""" + # Arrange + for key, value in environs.items(): + monkeypatch.setenv(key, value) + + # Assert / Act + with pytest.raises( + SyncSettingsValidationError, + match=(r"Could not validate environment variables settings:\n"), + ): + RedisSyncBackend() diff --git a/.conflict-side-1/tests/sync/utils.py b/.conflict-side-1/tests/sync/utils.py new file mode 100644 index 0000000..e20356b --- /dev/null +++ b/.conflict-side-1/tests/sync/utils.py @@ -0,0 +1,23 @@ +"""Test utilities for Lock.""" + +from anyio import Event, create_task_group, fail_after + +from grelmicro.sync._base import BaseLock + + +async def wait_first_acquired(locks: list[BaseLock]) -> None: + """Wait for the first lock to be acquired.""" + + async def wrapper(lock: BaseLock, event: Event) -> None: + """Send event when lock is acquired.""" + with fail_after(1): + await lock.acquire() + event.set() + + with fail_after(1): + async with create_task_group() as task_group: + event = Event() + for lock in locks: + task_group.start_soon(wrapper, lock, event) + await event.wait() + task_group.cancel_scope.cancel() diff --git a/.conflict-side-1/tests/task/__init__.py b/.conflict-side-1/tests/task/__init__.py new file mode 100644 index 0000000..ebf85b3 --- /dev/null +++ b/.conflict-side-1/tests/task/__init__.py @@ -0,0 +1 @@ +"""Grelmicro Task Scheduler Tests.""" diff --git a/.conflict-side-1/tests/task/samples.py b/.conflict-side-1/tests/task/samples.py new file mode 100644 index 0000000..d19c153 --- /dev/null +++ b/.conflict-side-1/tests/task/samples.py @@ -0,0 +1,86 @@ +"""Test Samples for the Task Component.""" + +from types import TracebackType +from typing import Self + +from anyio import TASK_STATUS_IGNORED, Condition, Event +from anyio.abc import TaskStatus +from typer import echo + +from grelmicro.sync.abc import Synchronization +from grelmicro.task.abc import Task + +condition = Condition() + + +def test1() -> None: + """Test Function.""" + echo("test1") + + +def test2() -> None: + """Test Function.""" + + +def test3(test: str = "test") -> None: + """Test Function.""" + + +async def notify() -> None: + """Test Function that notifies the condition.""" + async with condition: + condition.notify() + + +async def always_fail() -> None: + """Test Function that always fails.""" + msg = "Test Error" + raise ValueError(msg) + + +class SimpleClass: + """Test Class.""" + + def method(self) -> None: + """Test Method.""" + + @staticmethod + def static_method() -> None: + """Test Static Method.""" + + +class EventTask(Task): + """Test Scheduled Task with Event.""" + + def __init__(self, *, event: Event | None = None) -> None: + """Initialize the event task.""" + self._event = event or Event() + + @property + def name(self) -> str: + """Return the task name.""" + return "event_task" + + async def __call__( + self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED + ) -> None: + """Run the task that sets the event.""" + task_status.started() + self._event.set() + + +class BadLock(Synchronization): + """Bad Lock.""" + + async def __aenter__(self) -> Self: + """Enter the synchronization primitive.""" + msg = "Bad Lock" + raise ValueError(msg) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + """Exit the synchronization primitive.""" diff --git a/.conflict-side-1/tests/task/test_interval.py b/.conflict-side-1/tests/task/test_interval.py new file mode 100644 index 0000000..308d456 --- /dev/null +++ b/.conflict-side-1/tests/task/test_interval.py @@ -0,0 +1,127 @@ +"""Test Interval Task.""" + +import pytest +from anyio import create_task_group, sleep, sleep_forever +from pytest_mock import MockFixture + +from grelmicro.task._interval import IntervalTask +from tests.task.samples import ( + BadLock, + always_fail, + condition, + notify, + test1, +) + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] + +INTERVAL = 0.1 +SLEEP = 0.01 + + +def test_interval_task_init() -> None: + """Test Interval Task Initialization.""" + # Act + task = IntervalTask(interval=1, function=test1) + # Assert + assert task.name == "tests.task.samples:test1" + + +def test_interval_task_init_with_name() -> None: + """Test Interval Task Initialization with Name.""" + # Act + task = IntervalTask(interval=1, function=test1, name="test1") + # Assert + assert task.name == "test1" + + +def test_interval_task_init_with_invalid_interval() -> None: + """Test Interval Task Initialization with Invalid Interval.""" + # Act / Assert + with pytest.raises(ValueError, match="Interval must be greater than 0"): + IntervalTask(interval=0, function=test1) + + +async def test_interval_task_start() -> None: + """Test Interval Task Start.""" + # Arrange + task = IntervalTask(interval=1, function=notify) + # Act + async with create_task_group() as tg: + await tg.start(task) + async with condition: + await condition.wait() + tg.cancel_scope.cancel() + + +async def test_interval_task_execution_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test Interval Task Execution Error.""" + # Arrange + task = IntervalTask(interval=1, function=always_fail) + # Act + async with create_task_group() as tg: + await tg.start(task) + await sleep(SLEEP) + tg.cancel_scope.cancel() + + # Assert + assert any( + "Task execution error:" in record.message + for record in caplog.records + if record.levelname == "ERROR" + ) + + +async def test_interval_task_synchronization_error( + caplog: pytest.LogCaptureFixture, +) -> None: + """Test Interval Task Synchronization Error.""" + # Arrange + task = IntervalTask(interval=1, function=notify, sync=BadLock()) + + # Act + async with create_task_group() as tg: + await tg.start(task) + await sleep(SLEEP) + tg.cancel_scope.cancel() + + # Assert + assert any( + "Task synchronization error:" in record.message + for record in caplog.records + if record.levelname == "ERROR" + ) + + +async def test_interval_stop( + caplog: pytest.LogCaptureFixture, mocker: MockFixture +) -> None: + """Test Interval Task stop.""" + # Arrange + caplog.set_level("INFO") + + class CustomBaseException(BaseException): + pass + + mocker.patch( + "grelmicro.task._interval.sleep", side_effect=CustomBaseException + ) + task = IntervalTask(interval=1, function=test1) + + async def leader_election_during_runtime_error() -> None: + async with create_task_group() as tg: + await tg.start(task) + await sleep_forever() + + # Act + with pytest.raises(BaseExceptionGroup): + await leader_election_during_runtime_error() + + # Assert + assert any( + "Task stopped:" in record.message + for record in caplog.records + if record.levelname == "INFO" + ) diff --git a/.conflict-side-1/tests/task/test_manager.py b/.conflict-side-1/tests/task/test_manager.py new file mode 100644 index 0000000..62c9859 --- /dev/null +++ b/.conflict-side-1/tests/task/test_manager.py @@ -0,0 +1,81 @@ +"""Test Task Manager.""" + +import pytest +from anyio import Event + +from grelmicro.errors import OutOfContextError +from grelmicro.task import TaskManager +from grelmicro.task.errors import TaskAddOperationError +from tests.task.samples import EventTask + +pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] + + +def test_task_manager_init() -> None: + """Test Task Manager Initialization.""" + # Act + task = EventTask() + app = TaskManager() + app_with_tasks = TaskManager(tasks=[task]) + # Assert + assert app.tasks == [] + assert app_with_tasks.tasks == [task] + + +async def test_task_manager_context() -> None: + """Test Task Manager Context.""" + # Arrange + event = Event() + task = EventTask(event=event) + app = TaskManager(tasks=[task]) + + # Act + event_before = event.is_set() + async with app: + event_in_context = event.is_set() + + # Assert + assert event_before is False + assert event_in_context is True + + +@pytest.mark.parametrize("auto_start", [True, False]) +async def test_task_manager_auto_start_disabled(*, auto_start: bool) -> None: + """Test Task Manager Auto Start Disabled.""" + # Arrange + event = Event() + task = EventTask(event=event) + app = TaskManager(auto_start=auto_start, tasks=[task]) + + # Act + event_before = event.is_set() + async with app: + event_in_context = event.is_set() + + # Assert + assert event_before is False + assert event_in_context is auto_start + + +async def test_task_manager_already_started_error() -> None: + """Test Task Manager Already Started Warning.""" + # Arrange + app = TaskManager() + + # Act / Assert + async with app: + with pytest.raises(TaskAddOperationError): + await app.start() + + +async def test_task_manager_out_of_context_errors() -> None: + """Test Task Manager Out of Context Errors.""" + # Arrange + app = TaskManager() + + # Act / Assert + with pytest.raises(OutOfContextError): + await app.start() + + with pytest.raises(OutOfContextError): + await app.__aexit__(None, None, None) diff --git a/.conflict-side-1/tests/task/test_router.py b/.conflict-side-1/tests/task/test_router.py new file mode 100644 index 0000000..ed30af7 --- /dev/null +++ b/.conflict-side-1/tests/task/test_router.py @@ -0,0 +1,175 @@ +"""Test Task Router.""" + +from functools import partial + +import pytest + +from grelmicro.sync.lock import Lock +from grelmicro.sync.memory import MemorySyncBackend +from grelmicro.task import TaskRouter +from grelmicro.task._interval import IntervalTask +from grelmicro.task.errors import FunctionTypeError, TaskAddOperationError +from tests.task.samples import EventTask, SimpleClass, test1, test2, test3 + + +def test_router_init() -> None: + """Test Task Router Initialization.""" + # Arrange + custom_task = EventTask() + + # Act + router = TaskRouter() + router_with_task = TaskRouter(tasks=[custom_task]) + + # Assert + assert router.tasks == [] + assert router_with_task.tasks == [custom_task] + + +def test_router_add_task() -> None: + """Test Task Router Add Task.""" + # Arrange + custom_task1 = EventTask() + custom_task2 = EventTask() + router = TaskRouter() + router_with_task = TaskRouter(tasks=[custom_task1]) + + # Act + router.add_task(custom_task1) + router_with_task.add_task(custom_task2) + + # Assert + assert router.tasks == [custom_task1] + assert router_with_task.tasks == [custom_task1, custom_task2] + + +def test_router_include_router() -> None: + """Test Task Router Include Router.""" + # Arrange + custom_task1 = EventTask() + custom_task2 = EventTask() + router = TaskRouter(tasks=[custom_task1]) + router_with_task = TaskRouter(tasks=[custom_task2]) + + # Act + router.include_router(router_with_task) + + # Assert + assert router.tasks == [custom_task1, custom_task2] + + +def test_router_interval() -> None: + """Test Task Router add interval task.""" + # Arrange + task_count = 4 + custom_task = EventTask() + router = TaskRouter(tasks=[custom_task]) + sync = Lock(backend=MemorySyncBackend(), name="testlock") + + # Act + router.interval(name="test1", seconds=10, sync=sync)(test1) + router.interval(name="test2", seconds=20)(test2) + router.interval(seconds=10)(test3) + + # Assert + assert len(router.tasks) == task_count + assert ( + sum(isinstance(task, IntervalTask) for task in router.tasks) + == task_count - 1 + ) + assert router.tasks[0].name == "event_task" + assert router.tasks[1].name == "test1" + assert router.tasks[2].name == "test2" + assert router.tasks[3].name == "tests.task.samples:test3" + + +def test_router_interval_name_generation() -> None: + """Test Task Router Interval Name Generation.""" + # Arrange + router = TaskRouter() + + # Act + router.interval(seconds=10)(test1) + router.interval(seconds=10)(SimpleClass.static_method) + router.interval(seconds=10)(SimpleClass.method) + + # Assert + assert router.tasks[0].name == "tests.task.samples:test1" + assert ( + router.tasks[1].name == "tests.task.samples:SimpleClass.static_method" + ) + assert router.tasks[2].name == "tests.task.samples:SimpleClass.method" + + +def test_router_interval_name_generation_error() -> None: + """Test Task Router Interval Name Generation Error.""" + # Arrange + router = TaskRouter() + test_instance = SimpleClass() + + # Act + with pytest.raises(FunctionTypeError, match="nested function"): + + @router.interval(seconds=10) + def nested_function() -> None: + pass + + with pytest.raises(FunctionTypeError, match="lambda"): + router.interval(seconds=10)(lambda _: None) + + with pytest.raises(FunctionTypeError, match="method"): + router.interval(seconds=10)(test_instance.method) + + with pytest.raises(FunctionTypeError, match="partial()"): + router.interval(seconds=10)(partial(test1)) + + with pytest.raises( + FunctionTypeError, + match="callable without __module__ or __qualname__ attribute", + ): + router.interval(seconds=10)(object()) # type: ignore[arg-type] + + +def test_router_add_task_when_started() -> None: + """Test Task Router Add Task When Started.""" + # Arrange + custom_task = EventTask() + router = TaskRouter() + router.do_mark_as_started() + + # Act + with pytest.raises(TaskAddOperationError): + router.add_task(custom_task) + + +def test_router_include_router_when_started() -> None: + """Test Task Router Include Router When Started.""" + # Arrange + router = TaskRouter() + router.do_mark_as_started() + router_child = TaskRouter() + + # Act + with pytest.raises(TaskAddOperationError): + router.include_router(router_child) + + +def test_router_started_propagation() -> None: + """Test Task Router Started Propagation.""" + # Arrange + router = TaskRouter() + router_child = TaskRouter() + router.include_router(router_child) + + # Act + router_started_before = router.started() + router_child_started_before = router_child.started() + router.do_mark_as_started() + router_started_after = router.started() + router_child_started_after = router_child.started() + + # Assert + assert router_started_before is False + assert router_child_started_before is False + assert router_started_after is True + assert router_child_started_after is True diff --git a/.conflict-side-1/uv.lock b/.conflict-side-1/uv.lock new file mode 100644 index 0000000..ff11a2b --- /dev/null +++ b/.conflict-side-1/uv.lock @@ -0,0 +1,1934 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version < '3.13'", + "python_full_version >= '3.13'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.6.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, +] + +[[package]] +name = "async-timeout" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, + { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, + { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, + { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, + { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, + { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, + { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, + { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, + { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, + { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, + { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, + { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, + { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, + { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, + { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, + { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, +] + +[[package]] +name = "babel" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, + { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, + { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 }, + { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 }, + { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 }, + { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 }, + { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 }, + { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 }, + { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 }, + { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 }, + { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 }, + { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 }, + { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 }, + { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 }, + { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 }, + { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 }, + { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 }, + { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 }, + { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 }, + { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, + { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, + { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, + { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, + { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, + { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, + { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, + { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, + { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, + { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, + { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, + { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, + { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, + { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, + { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, + { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, + { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, + { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, + { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, + { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, + { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, + { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, + { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "43.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, + { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, + { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, + { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, + { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, + { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, + { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, + { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, + { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, + { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, + { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, + { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, +] + +[[package]] +name = "cyclic" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "docker" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, +] + +[[package]] +name = "fast-depends" +version = "2.4.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/f5/8b42b7588a67ad78991e5e7ca0e0c6a1ded535a69a725e4e48d3346a20c1/fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d", size = 16682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/08/4adb160d8394053289fdf3b276e93b53271fd463e54fff8911b23c1db4ed/fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2", size = 17651 }, +] + +[[package]] +name = "fastapi" +version = "0.115.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/29/f71316b9273b6552a263748e49cd7b83898dc9499a663d30c7b9cb853cb8/fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", size = 301047 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c4/148d5046a96c428464557264877ae5a9338a83bbe0df045088749ec89820/fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796", size = 94866 }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/1ad5ce32d029aeb9117e9a5a9b3e314a8477525d60c12a9b7730a3c186ec/fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", size = 15571 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/ea/4b5011012ac925fe2f83b19d0e09cee9d324141ec7bf5e78bb2817f96513/fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46", size = 9489 }, +] + +[[package]] +name = "faststream" +version = "0.5.30" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "fast-depends" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/d3/c2a3e1233274c93a4978cbac210a81ba05cee09e2e0051049b40f55406f1/faststream-0.5.30.tar.gz", hash = "sha256:50ad5288719cfa75c13e9c277d40afae62533a590facad6e6d215e868f2b97f4", size = 284478 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/ce/a9eec6c2c9803de6bc2b2a5cac35d56b8908c64fcdd4c73616c1a16c9b90/faststream-0.5.30-py3-none-any.whl", hash = "sha256:bf48826be99210f3e9c7dff1b2a17b4bc4762c873c5558ac81b9b873549ae6a1", size = 382011 }, +] + +[[package]] +name = "filelock" +version = "3.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, +] + +[[package]] +name = "grelmicro" +version = "0.2.2" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "fast-depends" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, +] + +[package.optional-dependencies] +postgres = [ + { name = "asyncpg" }, +] +redis = [ + { name = "redis" }, +] +standard = [ + { name = "loguru" }, + { name = "orjson" }, +] + +[package.dev-dependencies] +dev = [ + { name = "fastapi" }, + { name = "fastapi-cli" }, + { name = "faststream" }, + { name = "hatch" }, + { name = "mdx-include" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-randomly" }, + { name = "pytest-timeout" }, + { name = "ruff" }, + { name = "testcontainers", extra = ["redis"] }, +] +docs = [ + { name = "mkdocs-material" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "anyio", specifier = ">=4.0.0" }, + { name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.30.0" }, + { name = "fast-depends", specifier = ">=2.0.0" }, + { name = "loguru", marker = "extra == 'standard'", specifier = ">=0.7.2" }, + { name = "orjson", marker = "extra == 'standard'", specifier = ">=3.10.11" }, + { name = "pydantic", specifier = ">=2.5.0" }, + { name = "pydantic-settings", specifier = ">=2.5.0" }, + { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "fastapi", specifier = ">=0.115.5" }, + { name = "fastapi-cli", specifier = ">=0.0.5" }, + { name = "faststream", specifier = ">=0.5.30" }, + { name = "hatch", specifier = ">=1.13.0" }, + { name = "mdx-include", specifier = ">=1.4.2" }, + { name = "mypy", specifier = ">=1.12.0" }, + { name = "pre-commit", specifier = ">=4.0.1" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-mock", specifier = ">=3.14.0" }, + { name = "pytest-randomly", specifier = ">=3.16.0" }, + { name = "pytest-timeout", specifier = ">=2.3.1" }, + { name = "ruff", specifier = ">=0.7.4" }, + { name = "testcontainers", extras = ["postgres", "redis"], specifier = ">=4.8.2" }, +] +docs = [ + { name = "mkdocs-material", specifier = ">=9.5.44" }, + { name = "pygments", specifier = ">=2.18.0" }, + { name = "pymdown-extensions", specifier = ">=10.12" }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "hatch" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "hatchling" }, + { name = "httpx" }, + { name = "hyperlink" }, + { name = "keyring" }, + { name = "packaging" }, + { name = "pexpect" }, + { name = "platformdirs" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "tomli-w" }, + { name = "tomlkit" }, + { name = "userpath" }, + { name = "uv" }, + { name = "virtualenv" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/ed/5001de278f8d7381cbc84f5efdae72308fe37493bc063878f6a1ac07dab8/hatch-1.13.0.tar.gz", hash = "sha256:5e1a75770cfe8f3ebae3abfded3a976238b0acefd19cdabc5245597525b8066f", size = 5188060 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/8d/6d965a22bc38cec091ba82131624bb5d75471094d7fe05e829536de3de2f/hatch-1.13.0-py3-none-any.whl", hash = "sha256:bb1a18558a626279cae338b4d8a9d3ca4226d5e06d50de600608c57acd131b67", size = 125757 }, +] + +[[package]] +name = "hatchling" +version = "1.26.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "pathspec" }, + { name = "pluggy" }, + { name = "trove-classifiers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/47/7ec270a9567262ae3cb32dd420d2b53bf7aee769aca1f240eae0426b5bbc/hatchling-1.26.3.tar.gz", hash = "sha256:b672a9c36a601a06c4e88a1abb1330639ee8e721e0535a37536e546a667efc7a", size = 54968 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl", hash = "sha256:c407e1c6c17b574584a66ae60e8e9a01235ecb6dc61d01559bb936577aaf5846", size = 75773 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, + { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, + { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, + { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, + { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, + { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, + { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, + { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, +] + +[[package]] +name = "httpx" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, +] + +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, +] + +[[package]] +name = "identify" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/79/7a520fc5011e02ca3f3285b5f6820eaf80443eb73e3733f73c02fb42ba0b/identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd", size = 99113 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/86/c4395700f3c5475424fb5c41e20c16be28d10c904aee4d005ba3217fc8e7/identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", size = 98982 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, +] + +[[package]] +name = "jeepney" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "keyring" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/24/64447b13df6a0e2797b586dad715766d756c932ce8ace7f67bd384d76ae0/keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6", size = 62675 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/c9/353c156fa2f057e669106e5d6bcdecf85ef8d3536ce68ca96f18dc7b6d6f/keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741", size = 39096 }, +] + +[[package]] +name = "loguru" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549 }, +] + +[[package]] +name = "markdown" +version = "3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "mdx-include" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cyclic" }, + { name = "markdown" }, + { name = "rcslice" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591 }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, +] + +[[package]] +name = "mkdocs-material" +version = "9.5.44" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/56/182d8121db9ab553cdf9bc58d5972b89833f60b63272f693c1f2b849b640/mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0", size = 3964306 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/eb/a801d00e0e210d82184aacce596906ec065422c78a7319244ba0771c4ded/mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca", size = 8674509 }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, +] + +[[package]] +name = "more-itertools" +version = "10.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, +] + +[[package]] +name = "mypy" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/03/744330105a74dc004578f47ec27e1bf66b1dd5664ea444d18423e41343bd/mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d", size = 3150767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/0a/70de7c97a86cb85535077ab5cef1cbc4e2812fd2e9cc21d78eb561a6b80f/mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", size = 10940998 }, + { url = "https://files.pythonhosted.org/packages/c0/97/9ed6d4834d7549936ab88533b302184fb568a0940c4000d2aaee6dc07112/mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", size = 10108523 }, + { url = "https://files.pythonhosted.org/packages/48/41/1686f37d09c915dfc5b683e20cc99dabac199900b5ca6d22747b99ddcb50/mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", size = 12505553 }, + { url = "https://files.pythonhosted.org/packages/8d/2b/2dbcaa7e97b23f27ced77493256ee878f4a140ac750e198630ff1b9b60c6/mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", size = 12988634 }, + { url = "https://files.pythonhosted.org/packages/54/55/710d082e91a2ccaea21214229b11f9215a9d22446f949491b5457655e82b/mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", size = 9630747 }, + { url = "https://files.pythonhosted.org/packages/8a/74/b9e0e4f06e951e277058f878302faa154d282ca11274c59fe08353f52949/mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", size = 11079902 }, + { url = "https://files.pythonhosted.org/packages/9f/62/fcad290769db3eb0de265094cef5c94d6075c70bc1e42b67eee4ca192dcc/mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", size = 10072373 }, + { url = "https://files.pythonhosted.org/packages/cb/27/9ac78349c2952e4446288ec1174675ab9e0160ed18c2cb1154fa456c54e8/mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", size = 12589779 }, + { url = "https://files.pythonhosted.org/packages/7c/4a/58cebd122cf1cba95680ac51303fbeb508392413ca64e3e711aa7d4877aa/mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", size = 13044459 }, + { url = "https://files.pythonhosted.org/packages/5b/c7/672935e2a3f9bcc07b1b870395a653f665657bef3cdaa504ad99f56eadf0/mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", size = 9731919 }, + { url = "https://files.pythonhosted.org/packages/bb/b0/092be5094840a401940c95224f63bb2a8f09bce9251ac1df180ec523830c/mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", size = 11068611 }, + { url = "https://files.pythonhosted.org/packages/9a/86/f20f53b8f062876c39602243d7a59b5cabd6b24315d8de511d607fa4de6a/mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", size = 10068036 }, + { url = "https://files.pythonhosted.org/packages/84/c7/1dbd6575785522da1d4c1ac2c419505fcf23bee74811880cac447a4a77ab/mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", size = 12585671 }, + { url = "https://files.pythonhosted.org/packages/46/8a/f6ae18b446eb2bccce54c4bd94065bcfe417d6c67021dcc032bf1e720aff/mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", size = 13036083 }, + { url = "https://files.pythonhosted.org/packages/59/e6/fc65fde3dc7156fce8d49ba21c7b1f5d866ad50467bf196ca94a7f6d2c9e/mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", size = 9735467 }, + { url = "https://files.pythonhosted.org/packages/84/6b/1db9de4e0764778251fb2d64cb7455cf6db75dc99c9f72c8b7e74b6a8a17/mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", size = 2646060 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "orjson" +version = "3.10.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/3a/10320029954badc7eaa338a15ee279043436f396e965dafc169610e4933f/orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", size = 5444879 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/25/c869a1fbd481dcb02c70032fd6a7243de7582bc48c7cae03d6f0985a11c0/orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6", size = 266432 }, + { url = "https://files.pythonhosted.org/packages/6a/a4/2307155ee92457d28345308f7d8c0e712348404723025613adeffcb531d0/orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe", size = 151884 }, + { url = "https://files.pythonhosted.org/packages/aa/82/daf1b2596dd49fe44a1bd92367568faf6966dcb5d7f99fd437c3d0dc2de6/orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67", size = 167371 }, + { url = "https://files.pythonhosted.org/packages/63/a8/680578e4589be5fdcfe0186bdd7dc6fe4a39d30e293a9da833cbedd5a56e/orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b", size = 154368 }, + { url = "https://files.pythonhosted.org/packages/6e/ce/9cb394b5b01ef34579eeca6d704b21f97248f607067ce95a24ba9ea2698e/orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d", size = 165725 }, + { url = "https://files.pythonhosted.org/packages/49/24/55eeb05cfb36b9e950d05743e6f6fdb7d5f33ca951a27b06ea6d03371aed/orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5", size = 142522 }, + { url = "https://files.pythonhosted.org/packages/94/0c/3a6a289e56dcc9fe67dc6b6d33c91dc5491f9ec4a03745efd739d2acf0ff/orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a", size = 146934 }, + { url = "https://files.pythonhosted.org/packages/1d/5c/a08c0e90a91e2526029a4681ff8c6fc4495b8bab77d48801144e378c7da9/orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981", size = 142904 }, + { url = "https://files.pythonhosted.org/packages/2c/c9/710286a60b14e88288ca014d43befb08bb0a4a6a0f51b875f8c2f05e8205/orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55", size = 144459 }, + { url = "https://files.pythonhosted.org/packages/7d/68/ef7b920e0a09e02b1a30daca1b4864938463797995c2fabe457c1500220a/orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec", size = 136444 }, + { url = "https://files.pythonhosted.org/packages/78/f2/a712dbcef6d84ff53e13056e7dc69d9d4844bd1e35e51b7431679ddd154d/orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51", size = 266505 }, + { url = "https://files.pythonhosted.org/packages/94/54/53970831786d71f98fdc13c0f80451324c9b5c20fbf42f42ef6147607ee7/orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97", size = 151745 }, + { url = "https://files.pythonhosted.org/packages/35/38/482667da1ca7ef95d44d4d2328257a144fd2752383e688637c53ed474d2a/orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19", size = 167274 }, + { url = "https://files.pythonhosted.org/packages/23/2f/5bb0a03e819781d82dadb733fde8ebbe20d1777d1a33715d45ada4d82ce8/orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0", size = 154605 }, + { url = "https://files.pythonhosted.org/packages/49/e9/14cc34d45c7bd51665aff9b1bb6b83475a61c52edb0d753fffe1adc97764/orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433", size = 165874 }, + { url = "https://files.pythonhosted.org/packages/7b/61/c2781ecf90f99623e97c67a31e8553f38a1ecebaf3189485726ac8641576/orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5", size = 142813 }, + { url = "https://files.pythonhosted.org/packages/4d/4f/18c83f78b501b6608569b1610fcb5a25c9bb9ab6a7eb4b3a55131e0fba37/orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd", size = 146762 }, + { url = "https://files.pythonhosted.org/packages/ba/19/ea80d5b575abd3f76a790409c2b7b8a60f3fc9447965c27d09613b8bddf4/orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b", size = 143186 }, + { url = "https://files.pythonhosted.org/packages/2c/f5/d835fee01a0284d4b78effc24d16e7609daac2ff6b6851ca1bdd3b6194fc/orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d", size = 144489 }, + { url = "https://files.pythonhosted.org/packages/03/60/748e0e205060dec74328dfd835e47902eb5522ae011766da76bfff64e2f4/orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284", size = 136614 }, + { url = "https://files.pythonhosted.org/packages/13/92/400970baf46b987c058469e9e779fb7a40d54a5754914d3634cca417e054/orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", size = 266402 }, + { url = "https://files.pythonhosted.org/packages/3c/fa/f126fc2d817552bd1f67466205abdcbff64eab16f6844fe6df2853528675/orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", size = 140826 }, + { url = "https://files.pythonhosted.org/packages/ad/18/9b9664d7d4af5b4fe9fe6600b7654afc0684bba528260afdde10c4a530aa/orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", size = 142593 }, + { url = "https://files.pythonhosted.org/packages/20/f9/a30c68f12778d5e58e6b5cdd26f86ee2d0babce1a475073043f46fdd8402/orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", size = 146777 }, + { url = "https://files.pythonhosted.org/packages/f2/97/12047b0c0e9b391d589fb76eb40538f522edc664f650f8e352fdaaf77ff5/orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", size = 142961 }, + { url = "https://files.pythonhosted.org/packages/a4/97/d904e26c1cabf2dd6ab1b0909e9b790af28a7f0fcb9d8378d7320d4869eb/orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", size = 144486 }, + { url = "https://files.pythonhosted.org/packages/42/62/3760bd1e6e949321d99bab238d08db2b1266564d2f708af668f57109bb36/orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", size = 136361 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, +] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, + { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, + { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, + { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, + { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, + { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, + { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, + { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, + { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, + { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, + { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, + { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, + { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, + { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, + { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, + { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, + { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, + { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, + { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, + { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, + { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, + { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, + { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, + { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, + { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, + { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, + { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, + { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, + { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, + { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, + { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, + { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, + { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, + { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, + { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, +] + +[[package]] +name = "pygments" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/32f05854cfd432e9286bb41a870e0d1a926b72df5f5cdb6dec962b2e369e/pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7", size = 840790 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/32/95a164ddf533bd676cbbe878e36e89b4ade3efde8dd61d0148c90cbbe57e/pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", size = 263448 }, +] + +[[package]] +name = "pytest" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + +[[package]] +name = "pytest-mock" +version = "3.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, +] + +[[package]] +name = "pytest-randomly" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396 }, +] + +[[package]] +name = "pytest-timeout" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, +] + +[[package]] +name = "pywin32" +version = "308" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156 }, + { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559 }, + { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495 }, + { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, + { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, + { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, + { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, + { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, + { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, +] + +[[package]] +name = "rcslice" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180 }, +] + +[[package]] +name = "redis" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, +] + +[[package]] +name = "regex" +version = "2024.11.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, + { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, + { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, + { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, + { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, + { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, + { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, + { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, + { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, + { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, + { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, + { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, + { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, + { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, + { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, + { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, + { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, + { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, + { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, + { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, + { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, + { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, + { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, + { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, + { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, + { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, + { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, + { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, + { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, + { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, + { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, + { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, + { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, + { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, + { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, + { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, + { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, + { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, + { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, + { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, + { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, + { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, + { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, + { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "rich" +version = "13.9.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", size = 222889 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", size = 242157 }, +] + +[[package]] +name = "ruff" +version = "0.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, + { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, + { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, + { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, + { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, + { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, + { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, + { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, + { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, + { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, + { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, + { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, + { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, + { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, + { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, + { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "starlette" +version = "0.41.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, +] + +[[package]] +name = "testcontainers" +version = "4.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docker" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/72/c58d84f5704c6caadd9f803a3adad5ab54ac65328c02d13295f40860cf33/testcontainers-4.8.2.tar.gz", hash = "sha256:dd4a6a2ea09e3c3ecd39e180b6548105929d0bb78d665ce9919cb3f8c98f9853", size = 63590 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/77/5ac0dff2903a033d83d971fd85957356abdb66a327f3589df2b3d1a586b4/testcontainers-4.8.2-py3-none-any.whl", hash = "sha256:9e19af077cd96e1957c13ee466f1f32905bc6c5bc1bc98643eb18be1a989bfb0", size = 104326 }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "tomli" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, +] + +[[package]] +name = "tomli-w" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, +] + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + +[[package]] +name = "trove-classifiers" +version = "2024.10.21.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/85/92c2667cf221b37648041ce9319427f92fa76cbec634aad844e67e284706/trove_classifiers-2024.10.21.16.tar.gz", hash = "sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3", size = 16153 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl", hash = "sha256:0fb11f1e995a757807a8ef1c03829fbd4998d817319abcef1f33165750f103be", size = 13546 }, +] + +[[package]] +name = "typer" +version = "0.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "userpath" +version = "1.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065 }, +] + +[[package]] +name = "uv" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/ad/66cc8e00c217e7fcf76598c880632b480aa38d4cad311596b78e99737498/uv-0.5.4.tar.gz", hash = "sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00", size = 2315678 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/3e/6bf24d7bb0d11715ea783ecabcacdecdc8c51fca0144fcdad2090d65bae5/uv-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2", size = 13853445 }, + { url = "https://files.pythonhosted.org/packages/b8/be/c3acbe2944cd694a5d61a7a461468fa886512c84014545bb8f3244092eaa/uv-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58", size = 13969300 }, + { url = "https://files.pythonhosted.org/packages/1f/c5/06e3b93045179b92d75cf94e6e224baec3226070f1cbc0e11d4898300b54/uv-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4", size = 12932325 }, + { url = "https://files.pythonhosted.org/packages/b8/f9/06ab86e9f0c270c495077ef2b588458172ed84f9c337de725c8b08872354/uv-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39", size = 13183356 }, + { url = "https://files.pythonhosted.org/packages/c1/cb/bee01ef23e5020dc1f12d86ca8f82e95a723585db3ec64bfab4016e5616c/uv-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6", size = 13622310 }, + { url = "https://files.pythonhosted.org/packages/19/4b/128fd874151919c71af51f528db28964e6d8e509fff12210ec9ba99b13fb/uv-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263", size = 14207832 }, + { url = "https://files.pythonhosted.org/packages/b1/2b/0fed8a49440494f6806dcb67021ca8f14d46f45a665235fc153791e19574/uv-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83", size = 14878796 }, + { url = "https://files.pythonhosted.org/packages/c9/35/a6dc404d4d8884e26ad7bda004c101972fe7d81f86546a8628272812b897/uv-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25", size = 14687838 }, + { url = "https://files.pythonhosted.org/packages/74/9e/c2ebf66b90d48def06cda29626bb38068418ed135ca903beb293825ef66d/uv-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927", size = 18960541 }, + { url = "https://files.pythonhosted.org/packages/3d/67/28a8b4c23920ae1b1b0103ebae2fa176bd5677c4353b5e814a51bd183285/uv-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194", size = 14471756 }, + { url = "https://files.pythonhosted.org/packages/e9/1c/9698818f4c5493dfd5ab0899a90eee789cac214de2f171220bcdfaefc93a/uv-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01", size = 13389089 }, + { url = "https://files.pythonhosted.org/packages/0b/30/31a9985d84ffb63fb9212fa2b565497e0ceb581be055e5cc760afbe26b11/uv-0.5.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c", size = 13612748 }, + { url = "https://files.pythonhosted.org/packages/26/8d/bae613187ba88d74f0268246ce140f23d399bab96d2cbc055d6e4adafd09/uv-0.5.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876", size = 13946421 }, + { url = "https://files.pythonhosted.org/packages/0e/22/efd1eec81a566139bced68f4bd140c275edac3dac1bd6236cf8d756423db/uv-0.5.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119", size = 15752913 }, + { url = "https://files.pythonhosted.org/packages/49/b2/0cc4ae143b9605c25e75772aea22876b5875db79982ba62bb6f8d3099fab/uv-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc", size = 14599503 }, + { url = "https://files.pythonhosted.org/packages/51/9a/33d40a5068fd37c4f7b4fa82396e3ee90a691cd256f364ff398612c1d5d4/uv-0.5.4-py3-none-win32.whl", hash = "sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa", size = 13749570 }, + { url = "https://files.pythonhosted.org/packages/b1/c8/827e4da65cbdab2c1619767a68ab99a31de078e511b71ca9f24777df33f9/uv-0.5.4-py3-none-win_amd64.whl", hash = "sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb", size = 15573613 }, +] + +[[package]] +name = "uvicorn" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, + { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, + { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, + { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, + { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, + { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, + { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, +] + +[[package]] +name = "virtualenv" +version = "20.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838 }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, +] + +[[package]] +name = "watchfiles" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, + { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, + { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, + { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, + { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, + { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, + { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, + { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, + { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, + { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, + { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, + { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, + { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, + { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, + { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, + { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, + { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, + { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, + { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, + { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, + { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, + { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, + { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, + { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, + { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, + { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, + { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, + { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, + { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, + { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, + { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, + { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, + { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, + { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, + { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, + { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, + { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, + { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, +] + +[[package]] +name = "websockets" +version = "14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, + { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, + { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, + { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, + { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, + { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, + { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, + { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, + { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, + { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, + { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, + { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, + { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, + { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, + { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, + { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, + { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, + { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, + { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, + { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, + { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, + { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, + { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, + { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, + { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, + { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, + { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, + { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, + { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, + { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, + { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, + { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, + { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, +] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", size = 3676 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad", size = 3604 }, +] + +[[package]] +name = "wrapt" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313 }, + { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164 }, + { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890 }, + { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118 }, + { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746 }, + { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668 }, + { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556 }, + { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712 }, + { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327 }, + { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523 }, + { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614 }, + { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316 }, + { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322 }, + { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055 }, + { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291 }, + { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374 }, + { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896 }, + { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738 }, + { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568 }, + { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653 }, + { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, +] + +[[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 }, +] diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..2af04b7 --- /dev/null +++ b/README.txt @@ -0,0 +1 @@ +You have checked out a GitButler Conflicted commit. You probably didn't mean to do this. \ No newline at end of file From 4537a7d364646daebf144ea4be26bf8f9a476119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Thu, 28 Nov 2024 12:06:21 +0100 Subject: [PATCH 11/15] =?UTF-8?q?=E2=9C=85=20Improve=20PostgreSQL=20test?= =?UTF-8?q?=20URL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .conflict-base-0/.github/workflows/ci.yml | 94 - .../.github/workflows/release.yml | 110 - .conflict-base-0/.gitignore | 17 - .conflict-base-0/.pre-commit-config.yaml | 63 - .conflict-base-0/.vscode/settings.json | 58 - .conflict-base-0/LICENSE | 21 - .conflict-base-0/README.md | 158 -- .conflict-base-0/docs/index.md | 158 -- .conflict-base-0/docs/logging.md | 73 - .conflict-base-0/docs/sync.md | 81 - .conflict-base-0/docs/task.md | 85 - .conflict-base-0/examples/__init__.py | 1 - .conflict-base-0/examples/logging/__init__.py | 1 - .conflict-base-0/examples/logging/basic.log | 4 - .conflict-base-0/examples/logging/basic.py | 17 - .../examples/logging/configure_logging.py | 3 - .conflict-base-0/examples/logging/fastapi.py | 22 - .../examples/simple_fastapi_app.py | 54 - .conflict-base-0/examples/single_file_app.py | 114 - .conflict-base-0/examples/sync/__init__.py | 1 - .../examples/sync/leaderelection_anyio.py | 11 - .../examples/sync/leaderelection_task.py | 6 - .conflict-base-0/examples/sync/lock.py | 8 - .conflict-base-0/examples/sync/memory.py | 3 - .conflict-base-0/examples/sync/postgres.py | 3 - .conflict-base-0/examples/sync/redis.py | 3 - .conflict-base-0/examples/task/__init__.py | 1 - .conflict-base-0/examples/task/fastapi.py | 16 - .conflict-base-0/examples/task/faststream.py | 18 - .../examples/task/interval_manager.py | 8 - .../examples/task/interval_router.py | 8 - .../examples/task/leaderelection.py | 12 - .conflict-base-0/examples/task/lock.py | 11 - .conflict-base-0/examples/task/router.py | 15 - .conflict-base-0/grelmicro/__init__.py | 3 - .conflict-base-0/grelmicro/errors.py | 52 - .../grelmicro/logging/__init__.py | 5 - .conflict-base-0/grelmicro/logging/config.py | 43 - .conflict-base-0/grelmicro/logging/errors.py | 7 - .conflict-base-0/grelmicro/logging/loguru.py | 121 -- .conflict-base-0/grelmicro/py.typed | 0 .conflict-base-0/grelmicro/sync/__init__.py | 6 - .conflict-base-0/grelmicro/sync/_backends.py | 30 - .conflict-base-0/grelmicro/sync/_base.py | 101 - .conflict-base-0/grelmicro/sync/_utils.py | 38 - .conflict-base-0/grelmicro/sync/abc.py | 106 - .conflict-base-0/grelmicro/sync/errors.py | 67 - .../grelmicro/sync/leaderelection.py | 386 ---- .conflict-base-0/grelmicro/sync/lock.py | 324 --- .conflict-base-0/grelmicro/sync/memory.py | 78 - .conflict-base-0/grelmicro/sync/postgres.py | 204 -- .conflict-base-0/grelmicro/sync/redis.py | 146 -- .conflict-base-0/grelmicro/task/__init__.py | 6 - .conflict-base-0/grelmicro/task/_interval.py | 92 - .conflict-base-0/grelmicro/task/_utils.py | 43 - .conflict-base-0/grelmicro/task/abc.py | 31 - .conflict-base-0/grelmicro/task/errors.py | 28 - .conflict-base-0/grelmicro/task/manager.py | 89 - .conflict-base-0/grelmicro/task/router.py | 132 -- .conflict-base-0/mkdocs.yml | 47 - .conflict-base-0/pyproject.toml | 174 -- .conflict-base-0/tests/__init__.py | 1 - .conflict-base-0/tests/conftest.py | 9 - .conflict-base-0/tests/logging/__init__.py | 1 - .conflict-base-0/tests/logging/test_loguru.py | 274 --- .conflict-base-0/tests/sync/__init__.py | 1 - .conflict-base-0/tests/sync/test_backends.py | 370 ---- .../tests/sync/test_leaderelection.py | 457 ---- .conflict-base-0/tests/sync/test_lock.py | 506 ----- .conflict-base-0/tests/sync/test_postgres.py | 106 - .conflict-base-0/tests/sync/test_redis.py | 67 - .conflict-base-0/tests/sync/utils.py | 23 - .conflict-base-0/tests/task/__init__.py | 1 - .conflict-base-0/tests/task/samples.py | 86 - .conflict-base-0/tests/task/test_interval.py | 127 -- .conflict-base-0/tests/task/test_manager.py | 81 - .conflict-base-0/tests/task/test_router.py | 175 -- .conflict-base-0/uv.lock | 1934 ----------------- .conflict-files | 3 - .conflict-side-0/.github/workflows/ci.yml | 94 - .../.github/workflows/release.yml | 110 - .conflict-side-0/.gitignore | 17 - .conflict-side-0/.pre-commit-config.yaml | 63 - .conflict-side-0/.vscode/settings.json | 58 - .conflict-side-0/LICENSE | 21 - .conflict-side-0/README.md | 158 -- .conflict-side-0/docs/index.md | 158 -- .conflict-side-0/docs/logging.md | 73 - .conflict-side-0/docs/sync.md | 81 - .conflict-side-0/docs/task.md | 85 - .conflict-side-0/examples/__init__.py | 1 - .conflict-side-0/examples/logging/__init__.py | 1 - .conflict-side-0/examples/logging/basic.log | 4 - .conflict-side-0/examples/logging/basic.py | 17 - .../examples/logging/configure_logging.py | 3 - .conflict-side-0/examples/logging/fastapi.py | 22 - .../examples/simple_fastapi_app.py | 54 - .conflict-side-0/examples/single_file_app.py | 114 - .conflict-side-0/examples/sync/__init__.py | 1 - .../examples/sync/leaderelection_anyio.py | 11 - .../examples/sync/leaderelection_task.py | 6 - .conflict-side-0/examples/sync/lock.py | 8 - .conflict-side-0/examples/sync/memory.py | 3 - .conflict-side-0/examples/sync/postgres.py | 3 - .conflict-side-0/examples/sync/redis.py | 3 - .conflict-side-0/examples/task/__init__.py | 1 - .conflict-side-0/examples/task/fastapi.py | 16 - .conflict-side-0/examples/task/faststream.py | 18 - .../examples/task/interval_manager.py | 8 - .../examples/task/interval_router.py | 8 - .../examples/task/leaderelection.py | 12 - .conflict-side-0/examples/task/lock.py | 11 - .conflict-side-0/examples/task/router.py | 15 - .conflict-side-0/grelmicro/__init__.py | 3 - .conflict-side-0/grelmicro/errors.py | 52 - .../grelmicro/logging/__init__.py | 5 - .conflict-side-0/grelmicro/logging/config.py | 43 - .conflict-side-0/grelmicro/logging/errors.py | 7 - .conflict-side-0/grelmicro/logging/loguru.py | 121 -- .conflict-side-0/grelmicro/py.typed | 0 .conflict-side-0/grelmicro/sync/__init__.py | 6 - .conflict-side-0/grelmicro/sync/_backends.py | 30 - .conflict-side-0/grelmicro/sync/_base.py | 101 - .conflict-side-0/grelmicro/sync/_utils.py | 38 - .conflict-side-0/grelmicro/sync/abc.py | 106 - .conflict-side-0/grelmicro/sync/errors.py | 67 - .../grelmicro/sync/leaderelection.py | 386 ---- .conflict-side-0/grelmicro/sync/lock.py | 324 --- .conflict-side-0/grelmicro/sync/memory.py | 78 - .conflict-side-0/grelmicro/sync/postgres.py | 198 -- .conflict-side-0/grelmicro/sync/redis.py | 146 -- .conflict-side-0/grelmicro/task/__init__.py | 6 - .conflict-side-0/grelmicro/task/_interval.py | 92 - .conflict-side-0/grelmicro/task/_utils.py | 43 - .conflict-side-0/grelmicro/task/abc.py | 31 - .conflict-side-0/grelmicro/task/errors.py | 28 - .conflict-side-0/grelmicro/task/manager.py | 89 - .conflict-side-0/grelmicro/task/router.py | 132 -- .conflict-side-0/mkdocs.yml | 47 - .conflict-side-0/pyproject.toml | 174 -- .conflict-side-0/tests/__init__.py | 1 - .conflict-side-0/tests/conftest.py | 9 - .conflict-side-0/tests/logging/__init__.py | 1 - .conflict-side-0/tests/logging/test_loguru.py | 274 --- .conflict-side-0/tests/sync/__init__.py | 1 - .conflict-side-0/tests/sync/test_backends.py | 370 ---- .../tests/sync/test_leaderelection.py | 457 ---- .conflict-side-0/tests/sync/test_lock.py | 506 ----- .conflict-side-0/tests/sync/test_postgres.py | 106 - .conflict-side-0/tests/sync/test_redis.py | 67 - .conflict-side-0/tests/sync/utils.py | 23 - .conflict-side-0/tests/task/__init__.py | 1 - .conflict-side-0/tests/task/samples.py | 86 - .conflict-side-0/tests/task/test_interval.py | 127 -- .conflict-side-0/tests/task/test_manager.py | 81 - .conflict-side-0/tests/task/test_router.py | 175 -- .conflict-side-0/uv.lock | 1934 ----------------- .conflict-side-1/.github/workflows/ci.yml | 94 - .../.github/workflows/release.yml | 110 - .conflict-side-1/.gitignore | 17 - .conflict-side-1/.pre-commit-config.yaml | 63 - .conflict-side-1/.vscode/settings.json | 58 - .conflict-side-1/LICENSE | 21 - .conflict-side-1/README.md | 158 -- .conflict-side-1/docs/index.md | 158 -- .conflict-side-1/docs/logging.md | 73 - .conflict-side-1/docs/sync.md | 81 - .conflict-side-1/docs/task.md | 85 - .conflict-side-1/examples/__init__.py | 1 - .conflict-side-1/examples/logging/__init__.py | 1 - .conflict-side-1/examples/logging/basic.log | 4 - .conflict-side-1/examples/logging/basic.py | 17 - .../examples/logging/configure_logging.py | 3 - .conflict-side-1/examples/logging/fastapi.py | 22 - .../examples/simple_fastapi_app.py | 54 - .conflict-side-1/examples/single_file_app.py | 114 - .conflict-side-1/examples/sync/__init__.py | 1 - .../examples/sync/leaderelection_anyio.py | 11 - .../examples/sync/leaderelection_task.py | 6 - .conflict-side-1/examples/sync/lock.py | 8 - .conflict-side-1/examples/sync/memory.py | 3 - .conflict-side-1/examples/sync/postgres.py | 3 - .conflict-side-1/examples/sync/redis.py | 3 - .conflict-side-1/examples/task/__init__.py | 1 - .conflict-side-1/examples/task/fastapi.py | 16 - .conflict-side-1/examples/task/faststream.py | 18 - .../examples/task/interval_manager.py | 8 - .../examples/task/interval_router.py | 8 - .../examples/task/leaderelection.py | 12 - .conflict-side-1/examples/task/lock.py | 11 - .conflict-side-1/examples/task/router.py | 15 - .conflict-side-1/grelmicro/__init__.py | 3 - .conflict-side-1/grelmicro/errors.py | 52 - .../grelmicro/logging/__init__.py | 5 - .conflict-side-1/grelmicro/logging/config.py | 43 - .conflict-side-1/grelmicro/logging/errors.py | 7 - .conflict-side-1/grelmicro/logging/loguru.py | 121 -- .conflict-side-1/grelmicro/py.typed | 0 .conflict-side-1/grelmicro/sync/__init__.py | 6 - .conflict-side-1/grelmicro/sync/_backends.py | 30 - .conflict-side-1/grelmicro/sync/_base.py | 101 - .conflict-side-1/grelmicro/sync/_utils.py | 38 - .conflict-side-1/grelmicro/sync/abc.py | 106 - .conflict-side-1/grelmicro/sync/errors.py | 67 - .../grelmicro/sync/leaderelection.py | 386 ---- .conflict-side-1/grelmicro/sync/lock.py | 324 --- .conflict-side-1/grelmicro/sync/memory.py | 78 - .conflict-side-1/grelmicro/sync/postgres.py | 206 -- .conflict-side-1/grelmicro/sync/redis.py | 146 -- .conflict-side-1/grelmicro/task/__init__.py | 6 - .conflict-side-1/grelmicro/task/_interval.py | 92 - .conflict-side-1/grelmicro/task/_utils.py | 43 - .conflict-side-1/grelmicro/task/abc.py | 31 - .conflict-side-1/grelmicro/task/errors.py | 28 - .conflict-side-1/grelmicro/task/manager.py | 89 - .conflict-side-1/grelmicro/task/router.py | 132 -- .conflict-side-1/mkdocs.yml | 47 - .conflict-side-1/pyproject.toml | 174 -- .conflict-side-1/tests/__init__.py | 1 - .conflict-side-1/tests/conftest.py | 9 - .conflict-side-1/tests/logging/__init__.py | 1 - .conflict-side-1/tests/logging/test_loguru.py | 274 --- .conflict-side-1/tests/sync/__init__.py | 1 - .conflict-side-1/tests/sync/test_backends.py | 370 ---- .../tests/sync/test_leaderelection.py | 457 ---- .conflict-side-1/tests/sync/test_lock.py | 506 ----- .conflict-side-1/tests/sync/test_postgres.py | 106 - .conflict-side-1/tests/sync/test_redis.py | 67 - .conflict-side-1/tests/sync/utils.py | 23 - .conflict-side-1/tests/task/__init__.py | 1 - .conflict-side-1/tests/task/samples.py | 86 - .conflict-side-1/tests/task/test_interval.py | 127 -- .conflict-side-1/tests/task/test_manager.py | 81 - .conflict-side-1/tests/task/test_router.py | 175 -- .conflict-side-1/uv.lock | 1934 ----------------- .../.github => .github}/workflows/ci.yml | 0 .../.github => .github}/workflows/release.yml | 0 .auto-resolution/.gitignore => .gitignore | 0 ...mit-config.yaml => .pre-commit-config.yaml | 0 .../.vscode => .vscode}/settings.json | 0 .auto-resolution/LICENSE => LICENSE | 0 .auto-resolution/README.md => README.md | 0 README.txt | 1 - {.auto-resolution/docs => docs}/index.md | 0 {.auto-resolution/docs => docs}/logging.md | 0 {.auto-resolution/docs => docs}/sync.md | 0 {.auto-resolution/docs => docs}/task.md | 0 .../examples => examples}/__init__.py | 0 .../examples => examples}/logging/__init__.py | 0 .../examples => examples}/logging/basic.log | 0 .../examples => examples}/logging/basic.py | 0 .../logging/configure_logging.py | 0 .../examples => examples}/logging/fastapi.py | 0 .../simple_fastapi_app.py | 0 .../examples => examples}/single_file_app.py | 0 .../examples => examples}/sync/__init__.py | 0 .../sync/leaderelection_anyio.py | 0 .../sync/leaderelection_task.py | 0 .../examples => examples}/sync/lock.py | 0 .../examples => examples}/sync/memory.py | 0 .../examples => examples}/sync/postgres.py | 0 .../examples => examples}/sync/redis.py | 0 .../examples => examples}/task/__init__.py | 0 .../examples => examples}/task/fastapi.py | 0 .../examples => examples}/task/faststream.py | 0 .../task/interval_manager.py | 0 .../task/interval_router.py | 0 .../task/leaderelection.py | 0 .../examples => examples}/task/lock.py | 0 .../examples => examples}/task/router.py | 0 .../grelmicro => grelmicro}/__init__.py | 0 .../grelmicro => grelmicro}/errors.py | 0 .../logging/__init__.py | 0 .../grelmicro => grelmicro}/logging/config.py | 0 .../grelmicro => grelmicro}/logging/errors.py | 0 .../grelmicro => grelmicro}/logging/loguru.py | 0 .../grelmicro => grelmicro}/py.typed | 0 .../grelmicro => grelmicro}/sync/__init__.py | 0 .../grelmicro => grelmicro}/sync/_backends.py | 0 .../grelmicro => grelmicro}/sync/_base.py | 0 .../grelmicro => grelmicro}/sync/_utils.py | 0 .../grelmicro => grelmicro}/sync/abc.py | 0 .../grelmicro => grelmicro}/sync/errors.py | 0 .../sync/leaderelection.py | 0 .../grelmicro => grelmicro}/sync/lock.py | 0 .../grelmicro => grelmicro}/sync/memory.py | 0 .../grelmicro => grelmicro}/sync/postgres.py | 0 .../grelmicro => grelmicro}/sync/redis.py | 0 .../grelmicro => grelmicro}/task/__init__.py | 0 .../grelmicro => grelmicro}/task/_interval.py | 0 .../grelmicro => grelmicro}/task/_utils.py | 0 .../grelmicro => grelmicro}/task/abc.py | 0 .../grelmicro => grelmicro}/task/errors.py | 0 .../grelmicro => grelmicro}/task/manager.py | 0 .../grelmicro => grelmicro}/task/router.py | 0 .auto-resolution/mkdocs.yml => mkdocs.yml | 0 .../pyproject.toml => pyproject.toml | 0 {.auto-resolution/tests => tests}/__init__.py | 0 {.auto-resolution/tests => tests}/conftest.py | 0 .../tests => tests}/logging/__init__.py | 0 .../tests => tests}/logging/test_loguru.py | 0 .../tests => tests}/sync/__init__.py | 0 .../tests => tests}/sync/test_backends.py | 0 .../sync/test_leaderelection.py | 0 .../tests => tests}/sync/test_lock.py | 0 .../tests => tests}/sync/test_postgres.py | 19 +- .../tests => tests}/sync/test_redis.py | 0 .../tests => tests}/sync/utils.py | 0 .../tests => tests}/task/__init__.py | 0 .../tests => tests}/task/samples.py | 0 .../tests => tests}/task/test_interval.py | 0 .../tests => tests}/task/test_manager.py | 0 .../tests => tests}/task/test_router.py | 0 .auto-resolution/uv.lock => uv.lock | 0 314 files changed, 11 insertions(+), 23516 deletions(-) delete mode 100644 .conflict-base-0/.github/workflows/ci.yml delete mode 100644 .conflict-base-0/.github/workflows/release.yml delete mode 100644 .conflict-base-0/.gitignore delete mode 100644 .conflict-base-0/.pre-commit-config.yaml delete mode 100644 .conflict-base-0/.vscode/settings.json delete mode 100644 .conflict-base-0/LICENSE delete mode 100644 .conflict-base-0/README.md delete mode 100644 .conflict-base-0/docs/index.md delete mode 100644 .conflict-base-0/docs/logging.md delete mode 100644 .conflict-base-0/docs/sync.md delete mode 100644 .conflict-base-0/docs/task.md delete mode 100644 .conflict-base-0/examples/__init__.py delete mode 100644 .conflict-base-0/examples/logging/__init__.py delete mode 100644 .conflict-base-0/examples/logging/basic.log delete mode 100644 .conflict-base-0/examples/logging/basic.py delete mode 100644 .conflict-base-0/examples/logging/configure_logging.py delete mode 100644 .conflict-base-0/examples/logging/fastapi.py delete mode 100644 .conflict-base-0/examples/simple_fastapi_app.py delete mode 100644 .conflict-base-0/examples/single_file_app.py delete mode 100644 .conflict-base-0/examples/sync/__init__.py delete mode 100644 .conflict-base-0/examples/sync/leaderelection_anyio.py delete mode 100644 .conflict-base-0/examples/sync/leaderelection_task.py delete mode 100644 .conflict-base-0/examples/sync/lock.py delete mode 100644 .conflict-base-0/examples/sync/memory.py delete mode 100644 .conflict-base-0/examples/sync/postgres.py delete mode 100644 .conflict-base-0/examples/sync/redis.py delete mode 100644 .conflict-base-0/examples/task/__init__.py delete mode 100644 .conflict-base-0/examples/task/fastapi.py delete mode 100644 .conflict-base-0/examples/task/faststream.py delete mode 100644 .conflict-base-0/examples/task/interval_manager.py delete mode 100644 .conflict-base-0/examples/task/interval_router.py delete mode 100644 .conflict-base-0/examples/task/leaderelection.py delete mode 100644 .conflict-base-0/examples/task/lock.py delete mode 100644 .conflict-base-0/examples/task/router.py delete mode 100644 .conflict-base-0/grelmicro/__init__.py delete mode 100644 .conflict-base-0/grelmicro/errors.py delete mode 100644 .conflict-base-0/grelmicro/logging/__init__.py delete mode 100644 .conflict-base-0/grelmicro/logging/config.py delete mode 100644 .conflict-base-0/grelmicro/logging/errors.py delete mode 100644 .conflict-base-0/grelmicro/logging/loguru.py delete mode 100644 .conflict-base-0/grelmicro/py.typed delete mode 100644 .conflict-base-0/grelmicro/sync/__init__.py delete mode 100644 .conflict-base-0/grelmicro/sync/_backends.py delete mode 100644 .conflict-base-0/grelmicro/sync/_base.py delete mode 100644 .conflict-base-0/grelmicro/sync/_utils.py delete mode 100644 .conflict-base-0/grelmicro/sync/abc.py delete mode 100644 .conflict-base-0/grelmicro/sync/errors.py delete mode 100644 .conflict-base-0/grelmicro/sync/leaderelection.py delete mode 100644 .conflict-base-0/grelmicro/sync/lock.py delete mode 100644 .conflict-base-0/grelmicro/sync/memory.py delete mode 100644 .conflict-base-0/grelmicro/sync/postgres.py delete mode 100644 .conflict-base-0/grelmicro/sync/redis.py delete mode 100644 .conflict-base-0/grelmicro/task/__init__.py delete mode 100644 .conflict-base-0/grelmicro/task/_interval.py delete mode 100644 .conflict-base-0/grelmicro/task/_utils.py delete mode 100644 .conflict-base-0/grelmicro/task/abc.py delete mode 100644 .conflict-base-0/grelmicro/task/errors.py delete mode 100644 .conflict-base-0/grelmicro/task/manager.py delete mode 100644 .conflict-base-0/grelmicro/task/router.py delete mode 100644 .conflict-base-0/mkdocs.yml delete mode 100644 .conflict-base-0/pyproject.toml delete mode 100644 .conflict-base-0/tests/__init__.py delete mode 100644 .conflict-base-0/tests/conftest.py delete mode 100644 .conflict-base-0/tests/logging/__init__.py delete mode 100644 .conflict-base-0/tests/logging/test_loguru.py delete mode 100644 .conflict-base-0/tests/sync/__init__.py delete mode 100644 .conflict-base-0/tests/sync/test_backends.py delete mode 100644 .conflict-base-0/tests/sync/test_leaderelection.py delete mode 100644 .conflict-base-0/tests/sync/test_lock.py delete mode 100644 .conflict-base-0/tests/sync/test_postgres.py delete mode 100644 .conflict-base-0/tests/sync/test_redis.py delete mode 100644 .conflict-base-0/tests/sync/utils.py delete mode 100644 .conflict-base-0/tests/task/__init__.py delete mode 100644 .conflict-base-0/tests/task/samples.py delete mode 100644 .conflict-base-0/tests/task/test_interval.py delete mode 100644 .conflict-base-0/tests/task/test_manager.py delete mode 100644 .conflict-base-0/tests/task/test_router.py delete mode 100644 .conflict-base-0/uv.lock delete mode 100644 .conflict-files delete mode 100644 .conflict-side-0/.github/workflows/ci.yml delete mode 100644 .conflict-side-0/.github/workflows/release.yml delete mode 100644 .conflict-side-0/.gitignore delete mode 100644 .conflict-side-0/.pre-commit-config.yaml delete mode 100644 .conflict-side-0/.vscode/settings.json delete mode 100644 .conflict-side-0/LICENSE delete mode 100644 .conflict-side-0/README.md delete mode 100644 .conflict-side-0/docs/index.md delete mode 100644 .conflict-side-0/docs/logging.md delete mode 100644 .conflict-side-0/docs/sync.md delete mode 100644 .conflict-side-0/docs/task.md delete mode 100644 .conflict-side-0/examples/__init__.py delete mode 100644 .conflict-side-0/examples/logging/__init__.py delete mode 100644 .conflict-side-0/examples/logging/basic.log delete mode 100644 .conflict-side-0/examples/logging/basic.py delete mode 100644 .conflict-side-0/examples/logging/configure_logging.py delete mode 100644 .conflict-side-0/examples/logging/fastapi.py delete mode 100644 .conflict-side-0/examples/simple_fastapi_app.py delete mode 100644 .conflict-side-0/examples/single_file_app.py delete mode 100644 .conflict-side-0/examples/sync/__init__.py delete mode 100644 .conflict-side-0/examples/sync/leaderelection_anyio.py delete mode 100644 .conflict-side-0/examples/sync/leaderelection_task.py delete mode 100644 .conflict-side-0/examples/sync/lock.py delete mode 100644 .conflict-side-0/examples/sync/memory.py delete mode 100644 .conflict-side-0/examples/sync/postgres.py delete mode 100644 .conflict-side-0/examples/sync/redis.py delete mode 100644 .conflict-side-0/examples/task/__init__.py delete mode 100644 .conflict-side-0/examples/task/fastapi.py delete mode 100644 .conflict-side-0/examples/task/faststream.py delete mode 100644 .conflict-side-0/examples/task/interval_manager.py delete mode 100644 .conflict-side-0/examples/task/interval_router.py delete mode 100644 .conflict-side-0/examples/task/leaderelection.py delete mode 100644 .conflict-side-0/examples/task/lock.py delete mode 100644 .conflict-side-0/examples/task/router.py delete mode 100644 .conflict-side-0/grelmicro/__init__.py delete mode 100644 .conflict-side-0/grelmicro/errors.py delete mode 100644 .conflict-side-0/grelmicro/logging/__init__.py delete mode 100644 .conflict-side-0/grelmicro/logging/config.py delete mode 100644 .conflict-side-0/grelmicro/logging/errors.py delete mode 100644 .conflict-side-0/grelmicro/logging/loguru.py delete mode 100644 .conflict-side-0/grelmicro/py.typed delete mode 100644 .conflict-side-0/grelmicro/sync/__init__.py delete mode 100644 .conflict-side-0/grelmicro/sync/_backends.py delete mode 100644 .conflict-side-0/grelmicro/sync/_base.py delete mode 100644 .conflict-side-0/grelmicro/sync/_utils.py delete mode 100644 .conflict-side-0/grelmicro/sync/abc.py delete mode 100644 .conflict-side-0/grelmicro/sync/errors.py delete mode 100644 .conflict-side-0/grelmicro/sync/leaderelection.py delete mode 100644 .conflict-side-0/grelmicro/sync/lock.py delete mode 100644 .conflict-side-0/grelmicro/sync/memory.py delete mode 100644 .conflict-side-0/grelmicro/sync/postgres.py delete mode 100644 .conflict-side-0/grelmicro/sync/redis.py delete mode 100644 .conflict-side-0/grelmicro/task/__init__.py delete mode 100644 .conflict-side-0/grelmicro/task/_interval.py delete mode 100644 .conflict-side-0/grelmicro/task/_utils.py delete mode 100644 .conflict-side-0/grelmicro/task/abc.py delete mode 100644 .conflict-side-0/grelmicro/task/errors.py delete mode 100644 .conflict-side-0/grelmicro/task/manager.py delete mode 100644 .conflict-side-0/grelmicro/task/router.py delete mode 100644 .conflict-side-0/mkdocs.yml delete mode 100644 .conflict-side-0/pyproject.toml delete mode 100644 .conflict-side-0/tests/__init__.py delete mode 100644 .conflict-side-0/tests/conftest.py delete mode 100644 .conflict-side-0/tests/logging/__init__.py delete mode 100644 .conflict-side-0/tests/logging/test_loguru.py delete mode 100644 .conflict-side-0/tests/sync/__init__.py delete mode 100644 .conflict-side-0/tests/sync/test_backends.py delete mode 100644 .conflict-side-0/tests/sync/test_leaderelection.py delete mode 100644 .conflict-side-0/tests/sync/test_lock.py delete mode 100644 .conflict-side-0/tests/sync/test_postgres.py delete mode 100644 .conflict-side-0/tests/sync/test_redis.py delete mode 100644 .conflict-side-0/tests/sync/utils.py delete mode 100644 .conflict-side-0/tests/task/__init__.py delete mode 100644 .conflict-side-0/tests/task/samples.py delete mode 100644 .conflict-side-0/tests/task/test_interval.py delete mode 100644 .conflict-side-0/tests/task/test_manager.py delete mode 100644 .conflict-side-0/tests/task/test_router.py delete mode 100644 .conflict-side-0/uv.lock delete mode 100644 .conflict-side-1/.github/workflows/ci.yml delete mode 100644 .conflict-side-1/.github/workflows/release.yml delete mode 100644 .conflict-side-1/.gitignore delete mode 100644 .conflict-side-1/.pre-commit-config.yaml delete mode 100644 .conflict-side-1/.vscode/settings.json delete mode 100644 .conflict-side-1/LICENSE delete mode 100644 .conflict-side-1/README.md delete mode 100644 .conflict-side-1/docs/index.md delete mode 100644 .conflict-side-1/docs/logging.md delete mode 100644 .conflict-side-1/docs/sync.md delete mode 100644 .conflict-side-1/docs/task.md delete mode 100644 .conflict-side-1/examples/__init__.py delete mode 100644 .conflict-side-1/examples/logging/__init__.py delete mode 100644 .conflict-side-1/examples/logging/basic.log delete mode 100644 .conflict-side-1/examples/logging/basic.py delete mode 100644 .conflict-side-1/examples/logging/configure_logging.py delete mode 100644 .conflict-side-1/examples/logging/fastapi.py delete mode 100644 .conflict-side-1/examples/simple_fastapi_app.py delete mode 100644 .conflict-side-1/examples/single_file_app.py delete mode 100644 .conflict-side-1/examples/sync/__init__.py delete mode 100644 .conflict-side-1/examples/sync/leaderelection_anyio.py delete mode 100644 .conflict-side-1/examples/sync/leaderelection_task.py delete mode 100644 .conflict-side-1/examples/sync/lock.py delete mode 100644 .conflict-side-1/examples/sync/memory.py delete mode 100644 .conflict-side-1/examples/sync/postgres.py delete mode 100644 .conflict-side-1/examples/sync/redis.py delete mode 100644 .conflict-side-1/examples/task/__init__.py delete mode 100644 .conflict-side-1/examples/task/fastapi.py delete mode 100644 .conflict-side-1/examples/task/faststream.py delete mode 100644 .conflict-side-1/examples/task/interval_manager.py delete mode 100644 .conflict-side-1/examples/task/interval_router.py delete mode 100644 .conflict-side-1/examples/task/leaderelection.py delete mode 100644 .conflict-side-1/examples/task/lock.py delete mode 100644 .conflict-side-1/examples/task/router.py delete mode 100644 .conflict-side-1/grelmicro/__init__.py delete mode 100644 .conflict-side-1/grelmicro/errors.py delete mode 100644 .conflict-side-1/grelmicro/logging/__init__.py delete mode 100644 .conflict-side-1/grelmicro/logging/config.py delete mode 100644 .conflict-side-1/grelmicro/logging/errors.py delete mode 100644 .conflict-side-1/grelmicro/logging/loguru.py delete mode 100644 .conflict-side-1/grelmicro/py.typed delete mode 100644 .conflict-side-1/grelmicro/sync/__init__.py delete mode 100644 .conflict-side-1/grelmicro/sync/_backends.py delete mode 100644 .conflict-side-1/grelmicro/sync/_base.py delete mode 100644 .conflict-side-1/grelmicro/sync/_utils.py delete mode 100644 .conflict-side-1/grelmicro/sync/abc.py delete mode 100644 .conflict-side-1/grelmicro/sync/errors.py delete mode 100644 .conflict-side-1/grelmicro/sync/leaderelection.py delete mode 100644 .conflict-side-1/grelmicro/sync/lock.py delete mode 100644 .conflict-side-1/grelmicro/sync/memory.py delete mode 100644 .conflict-side-1/grelmicro/sync/postgres.py delete mode 100644 .conflict-side-1/grelmicro/sync/redis.py delete mode 100644 .conflict-side-1/grelmicro/task/__init__.py delete mode 100644 .conflict-side-1/grelmicro/task/_interval.py delete mode 100644 .conflict-side-1/grelmicro/task/_utils.py delete mode 100644 .conflict-side-1/grelmicro/task/abc.py delete mode 100644 .conflict-side-1/grelmicro/task/errors.py delete mode 100644 .conflict-side-1/grelmicro/task/manager.py delete mode 100644 .conflict-side-1/grelmicro/task/router.py delete mode 100644 .conflict-side-1/mkdocs.yml delete mode 100644 .conflict-side-1/pyproject.toml delete mode 100644 .conflict-side-1/tests/__init__.py delete mode 100644 .conflict-side-1/tests/conftest.py delete mode 100644 .conflict-side-1/tests/logging/__init__.py delete mode 100644 .conflict-side-1/tests/logging/test_loguru.py delete mode 100644 .conflict-side-1/tests/sync/__init__.py delete mode 100644 .conflict-side-1/tests/sync/test_backends.py delete mode 100644 .conflict-side-1/tests/sync/test_leaderelection.py delete mode 100644 .conflict-side-1/tests/sync/test_lock.py delete mode 100644 .conflict-side-1/tests/sync/test_postgres.py delete mode 100644 .conflict-side-1/tests/sync/test_redis.py delete mode 100644 .conflict-side-1/tests/sync/utils.py delete mode 100644 .conflict-side-1/tests/task/__init__.py delete mode 100644 .conflict-side-1/tests/task/samples.py delete mode 100644 .conflict-side-1/tests/task/test_interval.py delete mode 100644 .conflict-side-1/tests/task/test_manager.py delete mode 100644 .conflict-side-1/tests/task/test_router.py delete mode 100644 .conflict-side-1/uv.lock rename {.auto-resolution/.github => .github}/workflows/ci.yml (100%) rename {.auto-resolution/.github => .github}/workflows/release.yml (100%) rename .auto-resolution/.gitignore => .gitignore (100%) rename .auto-resolution/.pre-commit-config.yaml => .pre-commit-config.yaml (100%) rename {.auto-resolution/.vscode => .vscode}/settings.json (100%) rename .auto-resolution/LICENSE => LICENSE (100%) rename .auto-resolution/README.md => README.md (100%) delete mode 100644 README.txt rename {.auto-resolution/docs => docs}/index.md (100%) rename {.auto-resolution/docs => docs}/logging.md (100%) rename {.auto-resolution/docs => docs}/sync.md (100%) rename {.auto-resolution/docs => docs}/task.md (100%) rename {.auto-resolution/examples => examples}/__init__.py (100%) rename {.auto-resolution/examples => examples}/logging/__init__.py (100%) rename {.auto-resolution/examples => examples}/logging/basic.log (100%) rename {.auto-resolution/examples => examples}/logging/basic.py (100%) rename {.auto-resolution/examples => examples}/logging/configure_logging.py (100%) rename {.auto-resolution/examples => examples}/logging/fastapi.py (100%) rename {.auto-resolution/examples => examples}/simple_fastapi_app.py (100%) rename {.auto-resolution/examples => examples}/single_file_app.py (100%) rename {.auto-resolution/examples => examples}/sync/__init__.py (100%) rename {.auto-resolution/examples => examples}/sync/leaderelection_anyio.py (100%) rename {.auto-resolution/examples => examples}/sync/leaderelection_task.py (100%) rename {.auto-resolution/examples => examples}/sync/lock.py (100%) rename {.auto-resolution/examples => examples}/sync/memory.py (100%) rename {.auto-resolution/examples => examples}/sync/postgres.py (100%) rename {.auto-resolution/examples => examples}/sync/redis.py (100%) rename {.auto-resolution/examples => examples}/task/__init__.py (100%) rename {.auto-resolution/examples => examples}/task/fastapi.py (100%) rename {.auto-resolution/examples => examples}/task/faststream.py (100%) rename {.auto-resolution/examples => examples}/task/interval_manager.py (100%) rename {.auto-resolution/examples => examples}/task/interval_router.py (100%) rename {.auto-resolution/examples => examples}/task/leaderelection.py (100%) rename {.auto-resolution/examples => examples}/task/lock.py (100%) rename {.auto-resolution/examples => examples}/task/router.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/__init__.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/errors.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/logging/__init__.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/logging/config.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/logging/errors.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/logging/loguru.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/py.typed (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/__init__.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/_backends.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/_base.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/_utils.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/abc.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/errors.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/leaderelection.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/lock.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/memory.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/postgres.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/sync/redis.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/task/__init__.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/task/_interval.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/task/_utils.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/task/abc.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/task/errors.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/task/manager.py (100%) rename {.auto-resolution/grelmicro => grelmicro}/task/router.py (100%) rename .auto-resolution/mkdocs.yml => mkdocs.yml (100%) rename .auto-resolution/pyproject.toml => pyproject.toml (100%) rename {.auto-resolution/tests => tests}/__init__.py (100%) rename {.auto-resolution/tests => tests}/conftest.py (100%) rename {.auto-resolution/tests => tests}/logging/__init__.py (100%) rename {.auto-resolution/tests => tests}/logging/test_loguru.py (100%) rename {.auto-resolution/tests => tests}/sync/__init__.py (100%) rename {.auto-resolution/tests => tests}/sync/test_backends.py (100%) rename {.auto-resolution/tests => tests}/sync/test_leaderelection.py (100%) rename {.auto-resolution/tests => tests}/sync/test_lock.py (100%) rename {.auto-resolution/tests => tests}/sync/test_postgres.py (87%) rename {.auto-resolution/tests => tests}/sync/test_redis.py (100%) rename {.auto-resolution/tests => tests}/sync/utils.py (100%) rename {.auto-resolution/tests => tests}/task/__init__.py (100%) rename {.auto-resolution/tests => tests}/task/samples.py (100%) rename {.auto-resolution/tests => tests}/task/test_interval.py (100%) rename {.auto-resolution/tests => tests}/task/test_manager.py (100%) rename {.auto-resolution/tests => tests}/task/test_router.py (100%) rename .auto-resolution/uv.lock => uv.lock (100%) diff --git a/.conflict-base-0/.github/workflows/ci.yml b/.conflict-base-0/.github/workflows/ci.yml deleted file mode 100644 index 5fb99bb..0000000 --- a/.conflict-base-0/.github/workflows/ci.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: CI - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras - - - name: Run Mypy - run: uv run mypy . - - test: - name: Test Python ${{ matrix.python }} - runs-on: "ubuntu-latest" - strategy: - fail-fast: true - matrix: - python: ["3.11", "3.12", "3.13"] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras --python ${{ matrix.python }} - - - name: Run unit tests - run: uv run pytest -x - - - name: Run integration tests - run: uv run pytest -x -m integration --cov-append - - - name: Rename coverage report - run: mv .coverage .coverage.py${{ matrix.python }} - - - name: Save coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-${{ matrix.python }} - path: .coverage.py${{ matrix.python }} - include-hidden-files: true - - coverage-report: - name: Coverage report - runs-on: ubuntu-latest - needs: [test] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Get coverage reports - uses: actions/download-artifact@v4 - with: - pattern: coverage-* - merge-multiple: true - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras - - - name: Combine coverage reports - run: | - uv run coverage combine .coverage.* - uv run coverage xml -o cov.xml - - - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./cov.xml diff --git a/.conflict-base-0/.github/workflows/release.yml b/.conflict-base-0/.github/workflows/release.yml deleted file mode 100644 index c8d4bab..0000000 --- a/.conflict-base-0/.github/workflows/release.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Release - -on: - release: - types: - - published - -jobs: - bump-version: - name: Bump version - runs-on: ubuntu-latest - steps: - - - name: Generate GitHub App Token - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ secrets.GRELINFO_ID }} - private-key: ${{ secrets.GRELINFO_KEY }} - - - name: Get GitHub App User ID - id: user-id - run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - - - name: Configure Git App Credentials - run: | - git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' - git config --global user.email '${{ steps.user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}@users.noreply.github.com>' - - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref_name }} - token: ${{ steps.app-token.outputs.token }} - persist-credentials: false - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Get release version - id: release-version - run: echo "release-version=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" - - - name: Get current version - id: current-version - run: echo "current-version=$(uv run hatch version)" >> "$GITHUB_OUTPUT" - - - name: Bump version if necessary - if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} - run: | - uv run hatch version $RELEASE_VERSION - uv lock - - - name: Commit and push changes - if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} - run: | - git add . - git commit -m "🚀 Release $RELEASE_VERSION" - git tag -f $RELEASE_VERSION - git push origin $RELEASE_VERSION --force - git push origin HEAD:main - - publish-docs: - runs-on: ubuntu-latest - needs: [bump-version] - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.ref_name }} - - - name: Configure Git Credentials - run: | - git config user.name "${GITHUB_ACTOR}" - git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --group docs - - - name: Deploy docs on GitHub Pages - run: uv run mkdocs gh-deploy --force - - publish-pypi: - needs: [bump-version] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.ref_name }} - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Build - run: uv build - - - name: Publish - run: uv publish -t ${{ secrets.PYPI_TOKEN }} diff --git a/.conflict-base-0/.gitignore b/.conflict-base-0/.gitignore deleted file mode 100644 index 0d118ab..0000000 --- a/.conflict-base-0/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Python-generated files -__pycache__/ -*.py[oc] -build/ -dist/ -wheels/ -*.egg-info - -# Virtual environments -.venv - -# Coverage -cov.xml -.coverage - -# Mkdocs -site/ diff --git a/.conflict-base-0/.pre-commit-config.yaml b/.conflict-base-0/.pre-commit-config.yaml deleted file mode 100644 index 5e5a141..0000000 --- a/.conflict-base-0/.pre-commit-config.yaml +++ /dev/null @@ -1,63 +0,0 @@ -default_language_version: - python: python3.11 - -repos: - -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: end-of-file-fixer - - id: check-toml - - id: check-yaml - - id: check-added-large-files - - id: trailing-whitespace - -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 - hooks: - - id: ruff - args: [ --fix ] - - id: ruff-format - -- repo: https://github.com/codespell-project/codespell - rev: v2.3.0 - hooks: - - id: codespell - -- repo: local - hooks: - - - id: readme-to-docs - name: readme-to-docs - description: "Copy README.md to docs/index.md" - entry: cp README.md docs/index.md - language: system - pass_filenames: false - - # --- Local development hooks --- - - id: uv-lock - name: uv-lock - description: "Lock dependencies with 'uv lock'" - entry: uv lock - language: system - pass_filenames: false - - - id: mypy - name: mypy - description: "Run 'mypy' for static type checking" - entry: uv run mypy - language: system - types: [python] - require_serial: true - - - id: pytest - name: pytest - description: "Run 'pytest' for unit testing" - entry: uv run pytest --cov-fail-under=90 - language: system - pass_filenames: false - -ci: - autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate - skip: [uv-lock, mypy, pytest] diff --git a/.conflict-base-0/.vscode/settings.json b/.conflict-base-0/.vscode/settings.json deleted file mode 100644 index 806ffc4..0000000 --- a/.conflict-base-0/.vscode/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - // Editor settings - "editor.rulers": [80, 100], - "files.trimTrailingWhitespace": true, - "terminal.integrated.scrollback": 10000, - - // Files exclude settings - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "**/__pycache__": true, - "**/.venv": true, - "**/.mypy_cache": true, - "**/.pytest_cache": true, - "**/.ruff_cache": true, - ".coverage": true - }, - - // Python settings - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.terminal.activateEnvInCurrentTerminal": true, - "python.terminal.activateEnvironment": true, - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "python.testing.pytestArgs": ["--no-cov", "--color=yes"], - "python.analysis.inlayHints.pytestParameters": true, - - // Python editor settings - "[python]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff" - }, - - // Mypy settings - "mypy-type-checker.importStrategy": "fromEnvironment", - - // YAML settings - "yaml.schemas": { - "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" - }, - "yaml.customTags": [ - "!ENV scalar", - "!ENV sequence", - "!relative scalar", - "tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg", - "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", - "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", - "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" - ], - - // Ruff settings - "ruff.configurationPreference": "filesystemFirst" -} diff --git a/.conflict-base-0/LICENSE b/.conflict-base-0/LICENSE deleted file mode 100644 index 18dafa2..0000000 --- a/.conflict-base-0/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Loïc Gremaud - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/.conflict-base-0/README.md b/.conflict-base-0/README.md deleted file mode 100644 index 9f3e0ff..0000000 --- a/.conflict-base-0/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# Grelmicro - -Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. - -It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. - -[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) -[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) -[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) - -______________________________________________________________________ - -**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) - -**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) - -______________________________________________________________________ - -## Overview - -Grelmicro provides essential features for building robust distributed systems, including: - -- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. -- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. -- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. -- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. - -These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. - -### Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. - -### Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. - -The available primitives are: - -- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **Lock**: A distributed lock that can be used to synchronize access to shared resources. - -### Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Installation - -```bash -pip install grelmicro -``` - -## Examples - -### FastAPI Integration - -- Create a file `main.py` with: - -```python -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") -``` - -## Dependencies - -Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. - -### `standard` Dependencies - -When you install Grelmicro with `pip install grelmicro[standard]` it comes with: - -- `loguru`: A Python logging library. -- `orjson`: A fast, correct JSON library for Python. - -### `redis` Dependencies - -When you install Grelmicro with `pip install grelmicro[redis]` it comes with: - -- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). - -### `postgres` Dependencies - -When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: - -- `asyncpg`: The Python `asyncio` interface for PostgreSQL. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/.conflict-base-0/docs/index.md b/.conflict-base-0/docs/index.md deleted file mode 100644 index 9f3e0ff..0000000 --- a/.conflict-base-0/docs/index.md +++ /dev/null @@ -1,158 +0,0 @@ -# Grelmicro - -Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. - -It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. - -[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) -[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) -[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) - -______________________________________________________________________ - -**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) - -**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) - -______________________________________________________________________ - -## Overview - -Grelmicro provides essential features for building robust distributed systems, including: - -- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. -- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. -- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. -- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. - -These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. - -### Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. - -### Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. - -The available primitives are: - -- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **Lock**: A distributed lock that can be used to synchronize access to shared resources. - -### Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Installation - -```bash -pip install grelmicro -``` - -## Examples - -### FastAPI Integration - -- Create a file `main.py` with: - -```python -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") -``` - -## Dependencies - -Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. - -### `standard` Dependencies - -When you install Grelmicro with `pip install grelmicro[standard]` it comes with: - -- `loguru`: A Python logging library. -- `orjson`: A fast, correct JSON library for Python. - -### `redis` Dependencies - -When you install Grelmicro with `pip install grelmicro[redis]` it comes with: - -- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). - -### `postgres` Dependencies - -When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: - -- `asyncpg`: The Python `asyncio` interface for PostgreSQL. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/.conflict-base-0/docs/logging.md b/.conflict-base-0/docs/logging.md deleted file mode 100644 index 4575b03..0000000 --- a/.conflict-base-0/docs/logging.md +++ /dev/null @@ -1,73 +0,0 @@ -# Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to stdout. It supports JSON formatting and allows log level configuration via environment variables. - -## Dependencies - -For the moment the `logging` package is only working with the `loguru` Python logging library. -When `orjson` is installed, it will be used as the default JSON serializer for faster performance, otherwise, the standard `json` library will be used. - -[**Loguru**](https://loguru.readthedocs.io/en/stable/overview.html) is used as the logging library. - -For using `logging` package, please install the required dependencies: - -=== "Standard" - ```bash - pip install grelmicro[standard] - ``` - -=== "only loguru (minimum)" - ```bash - pip install loguru - ``` - -=== "loguru and orjson (manual)" - ```bash - pip install loguru orjson - ``` - - -## Configure Logging - -Just call the `configure_logging` function to set up the logging system. - -```python -{!> ../examples/logging/configure_logging.py!} -``` - -### Settings - -You can change the default settings using the following environment variables: - -- `LOG_LEVEL`: Set the desired log level (default: `INFO`). -- `LOG_FORMAT`: Choose the log format. Options are `TEXT` and `JSON`, or you can provide a custom [loguru](https://loguru.readthedocs.io/en/stable/overview.html) template (default: `TEXT`). - - -## Examples - -### Basic Usage - -Here is a quick example of how to use the logging system: - -```python -{!> ../examples/logging/basic.py!} -``` - -The console output, `stdout` will be: - -```json -{!> ../examples/logging/basic.log!} -``` - -### FastAPI Integration - -You can use the logging system with FastAPI as well: - -```python -{!> ../examples/logging/fastapi.py!} -``` - -!!! warning - It is crucial to call `configure_logging` during the lifespan of the FastAPI application. Failing to do so may result in the FastAPI CLI resetting the logging configuration. diff --git a/.conflict-base-0/docs/sync.md b/.conflict-base-0/docs/sync.md deleted file mode 100644 index 4c3b881..0000000 --- a/.conflict-base-0/docs/sync.md +++ /dev/null @@ -1,81 +0,0 @@ -# Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends (see more in the Backends section). - -The available primitives are: - -- **[Leader Election](#leader-election)**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **[Lock](#lock)**: A distributed lock that can be used to synchronize access to shared resources. - -The synchronization primitives can be used in combination with the `TaskManager` and `TaskRouter` to control task execution in a distributed system (see more in [Task Scheduler](task.md)). - -## Backend - -You must load a synchronization backend before using synchronization primitives. - -!!! note - Although Grelmicro use AnyIO for concurrency, the backends generally depend on `asyncio`, therefore Trio is not supported. - -You can initialize a backend like this: - -=== "Redis" - ```python - {!> ../examples/sync/redis.py!} - ``` - -=== "Postgres" - ```python - {!> ../examples/sync/postgres.py!} - ``` - -=== "Memory (For Testing Only)" - ```python - {!> ../examples/sync/memory.py!} - ``` - -!!! warning - Please make sure to use a proper way to store connection url, such as environment variables (not like the example above). - -!!! tip - Feel free to create your own backend and contribute it. In the `sync.abc` module, you can find the protocol for creating new backends. - - - -## Leader Election - -Leader election ensures that only one worker in the cluster is designated as the leader at any given time using a distributed lock. - -The leader election service is responsible for acquiring and renewing the distributed lock. It runs as an AnyIO Task that can be easily started with the [Task Manager](./task.md#task-manager). This service operates in the background, automatically renewing the lock to prevent other workers from acquiring it. The lock is released automatically when the task is cancelled or during shutdown. - -=== "Task Manager (Recommended)" - ```python - {!> ../examples/sync/leaderelection_task.py!} - ``` - -=== "AnyIO Task Group (Advanced)" - ```python - {!> ../examples/sync/leaderelection_anyio.py!} - ``` - -## Lock - -The lock is a distributed lock that can be used to synchronize access to shared resources. - -The lock supports the following features: - -- **Async**: The lock must be acquired and released asynchronously. -- **Distributed**: The lock must be distributed across multiple workers. -- **Reentrant**: The lock must allow the same token to acquire it multiple times to extend the lease. -- **Expiring**: The lock must have a timeout to auto-release after an interval to prevent deadlocks. -- **Non-blocking**: Lock operations must not block the async event loop. -- **Vendor-agnostic**: Must support multiple backends (Redis, Postgres, ConfigMap, etc.). - - -```python -{!> ../examples/sync/lock.py!} -``` - -!!! warning - The lock is designed for use within an async event loop and is not thread-safe or process-safe. diff --git a/.conflict-base-0/docs/task.md b/.conflict-base-0/docs/task.md deleted file mode 100644 index b6f0e00..0000000 --- a/.conflict-base-0/docs/task.md +++ /dev/null @@ -1,85 +0,0 @@ -# Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Task Manager - -The `TaskManager` class is the main entry point to manage scheduled tasks. You need to start the task manager to run the scheduled tasks using the application lifespan. - -=== "FastAPI" - - ```python - {!> ../examples/task/fastapi.py!} - ``` - -=== "FastStream" - - ```python - - {!> ../examples/task/faststream.py!} - ``` - -## Interval Task - -To create an `IntervalTask`, use the `interval` decorator method of the `TaskManager` instance. This decorator allows tasks to run at specified intervals. - -> **Note**: The interval specifies the waiting time between task executions. Ensure that the task execution duration is considered to meet deadlines effectively. - -=== "TaskManager" - - ```python - {!> ../examples/task/interval_manager.py!} - ``` - -=== "TaskRouter" - - ```python - {!> ../examples/task/interval_router.py!} - ``` - - -## Synchronization - -The Task can be synchronized using a [Synchoronization Primitive](sync.md) to control concurrency and manage simultaneous task execution. - -=== "Lock" - - ```python - {!> ../examples/task/lock.py!} - ``` - - -=== "Leader Election" - - - ```python - {!> ../examples/task/leaderelection.py!} - ``` - -## Task Router - -For bigger applications, you can use the `TaskRouter` class to manage tasks in different modules. - - -```python -{!> ../examples/task/router.py [ln:1-10]!} -``` - -Then you can include the `TaskRouter` into the `TaskManager` or other routers using the `include_router` method. - -```python -{!> ../examples/task/router.py [ln:12-]!} -``` - -!!! tip - The `TaskRouter` follows the same philosophy as the `APIRouter` in FastAPI or the **Router** in FastStream. diff --git a/.conflict-base-0/examples/__init__.py b/.conflict-base-0/examples/__init__.py deleted file mode 100644 index 73b7d32..0000000 --- a/.conflict-base-0/examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Examples.""" diff --git a/.conflict-base-0/examples/logging/__init__.py b/.conflict-base-0/examples/logging/__init__.py deleted file mode 100644 index bf04afe..0000000 --- a/.conflict-base-0/examples/logging/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Logging Examples.""" diff --git a/.conflict-base-0/examples/logging/basic.log b/.conflict-base-0/examples/logging/basic.log deleted file mode 100644 index 33c8e37..0000000 --- a/.conflict-base-0/examples/logging/basic.log +++ /dev/null @@ -1,4 +0,0 @@ -{"time":"2024-11-25T15:56:36.066922+01:00","level":"INFO","thread":"MainThread","logger":"__main__::7","msg":"This is an info message"} -{"time":"2024-11-25T15:56:36.067063+01:00","level":"WARNING","thread":"MainThread","logger":"__main__::8","msg":"This is a warning message with context","ctx":{"user":"Alice"}} -{"time":"2024-11-25T15:56:36.067105+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::9","msg":"This is an error message with context","ctx":{"user":"Bob"}} -{"time":"2024-11-25T15:56:36.067134+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::14","msg":"This is an exception message with context","ctx":{"user":"Charlie","exception":"ValueError: This is an exception"}} diff --git a/.conflict-base-0/examples/logging/basic.py b/.conflict-base-0/examples/logging/basic.py deleted file mode 100644 index 889f160..0000000 --- a/.conflict-base-0/examples/logging/basic.py +++ /dev/null @@ -1,17 +0,0 @@ -from loguru import logger - -from grelmicro.logging import configure_logging - -configure_logging() - -logger.debug("This is a debug message") -logger.info("This is an info message") -logger.warning("This is a warning message with context", user="Alice") -logger.error("This is an error message with context", user="Bob") - -try: - raise ValueError("This is an exception message") -except ValueError: - logger.exception( - "This is an exception message with context", user="Charlie" - ) diff --git a/.conflict-base-0/examples/logging/configure_logging.py b/.conflict-base-0/examples/logging/configure_logging.py deleted file mode 100644 index 0ffacd8..0000000 --- a/.conflict-base-0/examples/logging/configure_logging.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.logging import configure_logging - -configure_logging() diff --git a/.conflict-base-0/examples/logging/fastapi.py b/.conflict-base-0/examples/logging/fastapi.py deleted file mode 100644 index 7f318c5..0000000 --- a/.conflict-base-0/examples/logging/fastapi.py +++ /dev/null @@ -1,22 +0,0 @@ -from contextlib import asynccontextmanager - -from fastapi import FastAPI -from loguru import logger - -from grelmicro.logging import configure_logging - - -@asynccontextmanager -def lifespan_startup(): - # Ensure logging is configured during startup - configure_logging() - yield - - -app = FastAPI() - - -@app.get("/") -def root(): - logger.info("This is an info message") - return {"Hello": "World"} diff --git a/.conflict-base-0/examples/simple_fastapi_app.py b/.conflict-base-0/examples/simple_fastapi_app.py deleted file mode 100644 index ff52251..0000000 --- a/.conflict-base-0/examples/simple_fastapi_app.py +++ /dev/null @@ -1,54 +0,0 @@ -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") diff --git a/.conflict-base-0/examples/single_file_app.py b/.conflict-base-0/examples/single_file_app.py deleted file mode 100644 index 4f4bb87..0000000 --- a/.conflict-base-0/examples/single_file_app.py +++ /dev/null @@ -1,114 +0,0 @@ -import time -from contextlib import asynccontextmanager -from typing import Annotated - -import anyio -import typer -from fast_depends import Depends -from fastapi import FastAPI - -from grelmicro.sync.leaderelection import LeaderElection -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.task import TaskManager - -backend = MemorySyncBackend() -task = TaskManager() - - -@asynccontextmanager -async def lifespan(app): - async with backend, task: - typer.echo("App started") - yield - typer.echo("App stopped") - - -app = FastAPI(lifespan=lifespan) - -leased_lock_10sec = Lock( - name="leased_lock_10sec", - lease_duration=10, - backend=backend, -) -leased_lock_5sec = Lock( - name="leased_lock_5sec", - lease_duration=5, - backend=backend, -) - -leader_election = LeaderElection(name="simple-leader", backend=backend) - -task.add_task(leader_election) - - -@task.interval(seconds=1) -def sync_func_with_no_param(): - typer.echo("sync_with_no_param") - - -@task.interval(seconds=2) -async def async_func_with_no_param(): - typer.echo("async_with_no_param") - - -def sync_dependency(): - return "sync_dependency" - - -@task.interval(seconds=3) -def sync_func_with_sync_dependency( - sync_dependency: Annotated[str, Depends(sync_dependency)], -): - typer.echo(sync_dependency) - - -async def async_dependency(): - yield "async_with_async_dependency" - - -@task.interval(seconds=4) -async def async_func_with_async_dependency( - async_dependency: Annotated[str, Depends(async_dependency)], -): - typer.echo(async_dependency) - - -@task.interval(seconds=15, sync=leased_lock_10sec) -def sync_func_with_leased_lock_10sec(): - typer.echo("sync_func_with_leased_lock_10sec") - time.sleep(9) - - -@task.interval(seconds=15, sync=leased_lock_10sec) -async def async_func_with_leased_lock_10sec(): - typer.echo("async_func_with_leased_lock_10sec") - await anyio.sleep(9) - - -@task.interval(seconds=15, sync=leased_lock_5sec) -def sync_func_with_sync_dependency_and_leased_lock_5sec( - sync_dependency: Annotated[str, Depends(sync_dependency)], -): - typer.echo(sync_dependency) - time.sleep(4) - - -@task.interval(seconds=15, sync=leased_lock_5sec) -async def async_func_with_async_dependency_and_leased_lock_5sec( - async_dependency: Annotated[str, Depends(async_dependency)], -): - typer.echo(async_dependency) - await anyio.sleep(4) - - -@task.interval(seconds=15, sync=leader_election) -def sync_func_with_leader_election(): - typer.echo("sync_func_with_leader_election") - time.sleep(30) - - -@task.interval(seconds=15, sync=leader_election) -async def async_func_with_leader_election(): - typer.echo("async_func_with_leader_election") - await anyio.sleep(30) diff --git a/.conflict-base-0/examples/sync/__init__.py b/.conflict-base-0/examples/sync/__init__.py deleted file mode 100644 index acd409a..0000000 --- a/.conflict-base-0/examples/sync/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Synchronization Primitives Examples.""" diff --git a/.conflict-base-0/examples/sync/leaderelection_anyio.py b/.conflict-base-0/examples/sync/leaderelection_anyio.py deleted file mode 100644 index 784f188..0000000 --- a/.conflict-base-0/examples/sync/leaderelection_anyio.py +++ /dev/null @@ -1,11 +0,0 @@ -from anyio import create_task_group, sleep_forever - -from grelmicro.sync.leaderelection import LeaderElection - -leader = LeaderElection("cluster_group") - - -async def main(): - async with create_task_group() as tg: - await tg.start(leader) - await sleep_forever() diff --git a/.conflict-base-0/examples/sync/leaderelection_task.py b/.conflict-base-0/examples/sync/leaderelection_task.py deleted file mode 100644 index 58fa926..0000000 --- a/.conflict-base-0/examples/sync/leaderelection_task.py +++ /dev/null @@ -1,6 +0,0 @@ -from grelmicro.sync import LeaderElection -from grelmicro.task import TaskManager - -leader = LeaderElection("cluster_group") -task = TaskManager() -task.add_task(leader) diff --git a/.conflict-base-0/examples/sync/lock.py b/.conflict-base-0/examples/sync/lock.py deleted file mode 100644 index 7f38fe6..0000000 --- a/.conflict-base-0/examples/sync/lock.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.sync import Lock - -lock = Lock("resource_name") - - -async def main(): - async with lock: - print("Protected resource accessed") diff --git a/.conflict-base-0/examples/sync/memory.py b/.conflict-base-0/examples/sync/memory.py deleted file mode 100644 index 7eefea9..0000000 --- a/.conflict-base-0/examples/sync/memory.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.memory import MemorySyncBackend - -backend = MemorySyncBackend() diff --git a/.conflict-base-0/examples/sync/postgres.py b/.conflict-base-0/examples/sync/postgres.py deleted file mode 100644 index ea8b8c3..0000000 --- a/.conflict-base-0/examples/sync/postgres.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.postgres import PostgresSyncBackend - -backend = PostgresSyncBackend("postgresql://user:password@localhost:5432/db") diff --git a/.conflict-base-0/examples/sync/redis.py b/.conflict-base-0/examples/sync/redis.py deleted file mode 100644 index 0625f5d..0000000 --- a/.conflict-base-0/examples/sync/redis.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.redis import RedisSyncBackend - -backend = RedisSyncBackend("redis://localhost:6379/0") diff --git a/.conflict-base-0/examples/task/__init__.py b/.conflict-base-0/examples/task/__init__.py deleted file mode 100644 index 20f7752..0000000 --- a/.conflict-base-0/examples/task/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Task Scheduler Examples.""" diff --git a/.conflict-base-0/examples/task/fastapi.py b/.conflict-base-0/examples/task/fastapi.py deleted file mode 100644 index 16aaa8e..0000000 --- a/.conflict-base-0/examples/task/fastapi.py +++ /dev/null @@ -1,16 +0,0 @@ -from contextlib import asynccontextmanager - -from fastapi import FastAPI - -from grelmicro.task import TaskManager - -task = TaskManager() - - -@asynccontextmanager -async def lifespan(app: FastAPI): - async with task: - yield - - -app = FastAPI(lifespan=lifespan) diff --git a/.conflict-base-0/examples/task/faststream.py b/.conflict-base-0/examples/task/faststream.py deleted file mode 100644 index 688c8d9..0000000 --- a/.conflict-base-0/examples/task/faststream.py +++ /dev/null @@ -1,18 +0,0 @@ -from contextlib import asynccontextmanager - -from faststream import ContextRepo, FastStream -from faststream.redis import RedisBroker - -from grelmicro.task import TaskManager - -task = TaskManager() - - -@asynccontextmanager -async def lifespan(context: ContextRepo): - async with task: - yield - - -broker = RedisBroker() -app = FastStream(broker, lifespan=lifespan) diff --git a/.conflict-base-0/examples/task/interval_manager.py b/.conflict-base-0/examples/task/interval_manager.py deleted file mode 100644 index 91beb2e..0000000 --- a/.conflict-base-0/examples/task/interval_manager.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.task import TaskManager - -task = TaskManager() - - -@task.interval(seconds=5) -async def my_task(): - print("Hello, World!") diff --git a/.conflict-base-0/examples/task/interval_router.py b/.conflict-base-0/examples/task/interval_router.py deleted file mode 100644 index f114ad7..0000000 --- a/.conflict-base-0/examples/task/interval_router.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.task import TaskRouter - -task = TaskRouter() - - -@task.interval(seconds=5) -async def my_task(): - print("Hello, World!") diff --git a/.conflict-base-0/examples/task/leaderelection.py b/.conflict-base-0/examples/task/leaderelection.py deleted file mode 100644 index ad12773..0000000 --- a/.conflict-base-0/examples/task/leaderelection.py +++ /dev/null @@ -1,12 +0,0 @@ -from grelmicro.sync import LeaderElection -from grelmicro.task import TaskManager - -leader = LeaderElection("my_task") -task = TaskManager() -task.add_task(leader) - - -@task.interval(seconds=5, sync=leader) -async def my_task(): - async with leader: - print("Hello, World!") diff --git a/.conflict-base-0/examples/task/lock.py b/.conflict-base-0/examples/task/lock.py deleted file mode 100644 index cdbf795..0000000 --- a/.conflict-base-0/examples/task/lock.py +++ /dev/null @@ -1,11 +0,0 @@ -from grelmicro.sync import Lock -from grelmicro.task import TaskManager - -lock = Lock("my_task") -task = TaskManager() - - -@task.interval(seconds=5, sync=lock) -async def my_task(): - async with lock: - print("Hello, World!") diff --git a/.conflict-base-0/examples/task/router.py b/.conflict-base-0/examples/task/router.py deleted file mode 100644 index 2b166aa..0000000 --- a/.conflict-base-0/examples/task/router.py +++ /dev/null @@ -1,15 +0,0 @@ -from grelmicro.task import TaskRouter - - -router = TaskRouter() - - -@router.interval(seconds=5) -async def my_task(): - print("Hello, World!") - - -from grelmicro.task.manager import TaskManager - -task = TaskManager() -task.include_router(router) diff --git a/.conflict-base-0/grelmicro/__init__.py b/.conflict-base-0/grelmicro/__init__.py deleted file mode 100644 index 7cc6d82..0000000 --- a/.conflict-base-0/grelmicro/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python.""" # noqa: E501 - -__version__ = "0.2.2" diff --git a/.conflict-base-0/grelmicro/errors.py b/.conflict-base-0/grelmicro/errors.py deleted file mode 100644 index 141f82e..0000000 --- a/.conflict-base-0/grelmicro/errors.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Grelmicro Errors.""" - -from typing import assert_never - -from pydantic import ValidationError - - -class GrelmicroError(Exception): - """Base Grelmicro error.""" - - -class OutOfContextError(GrelmicroError, RuntimeError): - """Outside Context Error. - - Raised when a method is called outside of the context manager. - """ - - def __init__(self, cls: object, method_name: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not call {cls.__class__.__name__}.{method_name} outside of the context manager" - ) - - -class DependencyNotFoundError(GrelmicroError, ImportError): - """Dependency Not Found Error.""" - - def __init__(self, *, module: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not import module {module}, try running 'pip install {module}'" - ) - - -class SettingsValidationError(GrelmicroError, ValueError): - """Settings Validation Error.""" - - def __init__(self, error: ValidationError | str) -> None: - """Initialize the error.""" - if isinstance(error, str): - details = error - elif isinstance(error, ValidationError): - details = "\n".join( - f"- {data['loc'][0]}: {data['msg']} [input={data['input']}]" - for data in error.errors() - ) - else: - assert_never(error) - - super().__init__( - f"Could not validate environment variables settings:\n{details}" - ) diff --git a/.conflict-base-0/grelmicro/logging/__init__.py b/.conflict-base-0/grelmicro/logging/__init__.py deleted file mode 100644 index 60d3d45..0000000 --- a/.conflict-base-0/grelmicro/logging/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Grelmicro Logging.""" - -from grelmicro.logging.loguru import configure_logging - -__all__ = ["configure_logging"] diff --git a/.conflict-base-0/grelmicro/logging/config.py b/.conflict-base-0/grelmicro/logging/config.py deleted file mode 100644 index a6301c1..0000000 --- a/.conflict-base-0/grelmicro/logging/config.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Logging Configuration.""" - -from enum import StrEnum -from typing import Self - -from pydantic import Field -from pydantic_settings import BaseSettings - - -class _CaseInsensitiveEnum(StrEnum): - @classmethod - def _missing_(cls, value: object) -> Self | None: - value = str(value).lower() - for member in cls: - if member.lower() == value: - return member - return None - - -class LoggingLevelType(_CaseInsensitiveEnum): - """Logging Level Enum.""" - - DEBUG = "DEBUG" - INFO = "INFO" - WARNING = "WARNING" - ERROR = "ERROR" - CRITICAL = "CRITICAL" - - -class LoggingFormatType(_CaseInsensitiveEnum): - """Logging Format Enum.""" - - JSON = "JSON" - TEXT = "TEXT" - - -class LoggingSettings(BaseSettings): - """Logging Settings.""" - - LOG_LEVEL: LoggingLevelType = LoggingLevelType.INFO - LOG_FORMAT: LoggingFormatType | str = Field( - LoggingFormatType.JSON, union_mode="left_to_right" - ) diff --git a/.conflict-base-0/grelmicro/logging/errors.py b/.conflict-base-0/grelmicro/logging/errors.py deleted file mode 100644 index 097006f..0000000 --- a/.conflict-base-0/grelmicro/logging/errors.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Grelmicro Logging Errors.""" - -from grelmicro.errors import SettingsValidationError - - -class LoggingSettingsValidationError(SettingsValidationError): - """Logging Settings Validation Error.""" diff --git a/.conflict-base-0/grelmicro/logging/loguru.py b/.conflict-base-0/grelmicro/logging/loguru.py deleted file mode 100644 index a94202c..0000000 --- a/.conflict-base-0/grelmicro/logging/loguru.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Loguru Logging.""" - -import json -import sys -from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, NotRequired - -from pydantic import ValidationError -from typing_extensions import TypedDict - -from grelmicro.errors import DependencyNotFoundError -from grelmicro.logging.config import LoggingFormatType, LoggingSettings -from grelmicro.logging.errors import LoggingSettingsValidationError - -if TYPE_CHECKING: - from loguru import FormatFunction, Record - -try: - import loguru -except ImportError: # pragma: no cover - loguru = None # type: ignore[assignment] - -try: - import orjson - - def _json_dumps(obj: Mapping[str, Any]) -> str: - return orjson.dumps(obj).decode("utf-8") -except ImportError: # pragma: no cover - import json - - _json_dumps = json.dumps - - -JSON_FORMAT = "{extra[serialized]}" -TEXT_FORMAT = ( - "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " - "{name}:{function}:{line} - {message}" -) - - -class JSONRecordDict(TypedDict): - """JSON log record representation. - - The time use a ISO 8601 string. - """ - - time: str - level: str - msg: str - logger: str | None - thread: str - ctx: NotRequired[dict[Any, Any]] - - -def json_patcher(record: "Record") -> None: - """Patch the serialized log record with `JSONRecordDict` representation.""" - json_record = JSONRecordDict( - time=record["time"].isoformat(), - level=record["level"].name, - thread=record["thread"].name, - logger=f'{record["name"]}:{record["function"]}:{record["line"]}', - msg=record["message"], - ) - - ctx = {k: v for k, v in record["extra"].items() if k != "serialized"} - exception = record["exception"] - - if exception and exception.type: - ctx["exception"] = f"{exception.type.__name__}: {exception.value!s}" - - if ctx: - json_record["ctx"] = ctx - - record["extra"]["serialized"] = _json_dumps(json_record) - - -def json_formatter(record: "Record") -> str: - """Format log record with `JSONRecordDict` representation. - - This function does not return the formatted record directly but provides the format to use when - writing to the sink. - """ - json_patcher(record) - return JSON_FORMAT + "\n" - - -def configure_logging() -> None: - """Configure logging with loguru. - - Simple twelve-factor app logging configuration that logs to stdout. - - The following environment variables are used: - - LOG_LEVEL: The log level to use (default: INFO). - - LOG_FORMAT: JSON | TEXT or any loguru template to format logged message (default: JSON). - - Raises: - MissingDependencyError: If the loguru module is not installed. - LoggingSettingsError: If the LOG_FORMAT or LOG_LEVEL environment variable is invalid - """ - if not loguru: - raise DependencyNotFoundError(module="loguru") - - try: - settings = LoggingSettings() - except ValidationError as error: - raise LoggingSettingsValidationError(error) from None - - logger = loguru.logger - log_format: str | FormatFunction = settings.LOG_FORMAT - - if log_format is LoggingFormatType.JSON: - log_format = json_formatter - elif log_format is LoggingFormatType.TEXT: - log_format = TEXT_FORMAT - - logger.remove() - logger.add( - sys.stdout, - level=settings.LOG_LEVEL, - format=log_format, - ) diff --git a/.conflict-base-0/grelmicro/py.typed b/.conflict-base-0/grelmicro/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/.conflict-base-0/grelmicro/sync/__init__.py b/.conflict-base-0/grelmicro/sync/__init__.py deleted file mode 100644 index 128d56c..0000000 --- a/.conflict-base-0/grelmicro/sync/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Grelmicro Synchronization Primitives.""" - -from grelmicro.sync.leaderelection import LeaderElection -from grelmicro.sync.lock import Lock - -__all__ = ["LeaderElection", "Lock"] diff --git a/.conflict-base-0/grelmicro/sync/_backends.py b/.conflict-base-0/grelmicro/sync/_backends.py deleted file mode 100644 index 66f4b9f..0000000 --- a/.conflict-base-0/grelmicro/sync/_backends.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Grelmicro Backend Registry. - -Contains loaded backends of each type to be used as default. - -Note: - For now, only lock backends are supported, but other backends may be added in the future. -""" - -from typing import Literal, NotRequired, TypedDict - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import BackendNotLoadedError - - -class LoadedBackendsDict(TypedDict): - """Loaded backends type.""" - - lock: NotRequired[SyncBackend] - - -loaded_backends: LoadedBackendsDict = {} - - -def get_sync_backend() -> SyncBackend: - """Get the lock backend.""" - backend: Literal["lock"] = "lock" - try: - return loaded_backends[backend] - except KeyError: - raise BackendNotLoadedError(backend) from None diff --git a/.conflict-base-0/grelmicro/sync/_base.py b/.conflict-base-0/grelmicro/sync/_base.py deleted file mode 100644 index a0e6fb0..0000000 --- a/.conflict-base-0/grelmicro/sync/_base.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Grelmicro Lock API.""" - -from types import TracebackType -from typing import Annotated, Protocol, Self -from uuid import UUID - -from pydantic import BaseModel, ConfigDict -from typing_extensions import Doc - -from grelmicro.sync.abc import Synchronization - - -class BaseLockConfig(BaseModel): - """Base Lock Config.""" - - model_config = ConfigDict(frozen=True, extra="forbid") - - name: Annotated[ - str, - Doc(""" - The name of the resource to lock. - """), - ] - worker: Annotated[ - str | UUID, - Doc(""" - The worker identity. - - By default, use a UUIDv1. - """), - ] - - -class BaseLock(Synchronization, Protocol): - """Base Lock Protocol.""" - - async def __aenter__(self) -> Self: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - """ - ... - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - ... - - @property - def config(self) -> BaseLockConfig: - """Return the config.""" - ... - - async def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - ... - - async def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - WouldBlock: If the lock cannot be acquired without blocking. - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - ... - - async def release(self) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - ... - - async def locked(self) -> bool: - """Check if the lock is currently held.""" - ... - - async def owned(self) -> bool: - """Check if the lock is currently held by the current token.""" - ... diff --git a/.conflict-base-0/grelmicro/sync/_utils.py b/.conflict-base-0/grelmicro/sync/_utils.py deleted file mode 100644 index 2ad5dda..0000000 --- a/.conflict-base-0/grelmicro/sync/_utils.py +++ /dev/null @@ -1,38 +0,0 @@ -from threading import get_ident -from uuid import NAMESPACE_DNS, UUID, uuid3 - -from anyio import get_current_task - - -def generate_worker_namespace(worker: str) -> UUID: - """Generate a worker UUIDv3 namespace. - - Generate a worker UUID using UUIDv3 with the DNS namespace. - """ - return uuid3(namespace=NAMESPACE_DNS, name=worker) - - -def generate_task_token(worker: UUID | str) -> str: - """Generate a task UUID. - - The worker namespace is generated using `generate_worker_uuid` if the worker is a string. - Generate a task UUID using UUIDv3 with the worker namespace and the async task ID. - """ - worker = ( - generate_worker_namespace(worker) if isinstance(worker, str) else worker - ) - task = str(get_current_task().id) - return str(uuid3(namespace=worker, name=task)) - - -def generate_thread_token(worker: UUID | str) -> str: - """Generate a thread UUID. - - The worker namespace is generated using `generate_worker_uuid` if the worker is a string. - Generate a thread UUID using UUIDv3 with the worker namespace and the current thread ID. - """ - worker = ( - generate_worker_namespace(worker) if isinstance(worker, str) else worker - ) - thread = str(get_ident()) - return str(uuid3(namespace=worker, name=thread)) diff --git a/.conflict-base-0/grelmicro/sync/abc.py b/.conflict-base-0/grelmicro/sync/abc.py deleted file mode 100644 index 507477c..0000000 --- a/.conflict-base-0/grelmicro/sync/abc.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Grelmicro Synchronization Abstract Base Classes and Protocols.""" - -from types import TracebackType -from typing import Protocol, Self, runtime_checkable - -from pydantic import PositiveFloat - - -class SyncBackend(Protocol): - """Synchronization Backend Protocol. - - This is the low level API for the distributed lock backend that is platform agnostic. - """ - - async def __aenter__(self) -> Self: - """Open the lock backend.""" - ... - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Close the lock backend.""" - ... - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock. - - Args: - name: The name of the lock. - token: The token to acquire the lock. - duration: The duration in seconds to hold the lock. - - Returns: - True if the lock is acquired, False if the lock is already acquired by another token. - - Raises: - Exception: Any exception can be raised if the lock cannot be acquired. - """ - ... - - async def release(self, *, name: str, token: str) -> bool: - """Release a lock. - - Args: - name: The name of the lock. - token: The token to release the lock. - - Returns: - True if the lock was released, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock cannot be released. - """ - ... - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired. - - Args: - name: The name of the lock. - - Returns: - True if the lock is acquired, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock status cannot be checked. - """ - ... - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned. - - Args: - name: The name of the lock. - token: The token to check. - - Returns: - True if the lock is owned by the token, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock status cannot be checked. - """ - ... - - -@runtime_checkable -class Synchronization(Protocol): - """Synchronization Primitive Protocol.""" - - async def __aenter__(self) -> Self: - """Enter the synchronization primitive.""" - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the synchronization primitive.""" - ... - - -Seconds = PositiveFloat diff --git a/.conflict-base-0/grelmicro/sync/errors.py b/.conflict-base-0/grelmicro/sync/errors.py deleted file mode 100644 index 6384e36..0000000 --- a/.conflict-base-0/grelmicro/sync/errors.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Grelmicro Synchronization Primitive Errors.""" - -from grelmicro.errors import SettingsValidationError - - -class SyncError(Exception): - """Synchronization Primitive Error. - - This the base class for all lock errors. - """ - - -class SyncBackendError(SyncError): - """Synchronization Backend Error.""" - - -class BackendNotLoadedError(SyncBackendError): - """Backend Not Loaded Error.""" - - def __init__(self, backend_name: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not load backend {backend_name}, try initializing one first" - ) - - -class LockAcquireError(SyncBackendError): - """Acquire Lock Error. - - This error is raised when an error on backend side occurs during lock acquisition. - """ - - def __init__(self, *, name: str, token: str) -> None: - """Initialize the error.""" - super().__init__(f"Failed to acquire lock: name={name}, token={token}") - - -class LockReleaseError(SyncBackendError): - """Lock Release Error. - - This error is raised when an error on backend side occurs during lock release. - """ - - def __init__( - self, *, name: str, token: str, reason: str | None = None - ) -> None: - """Initialize the error.""" - super().__init__( - f"Failed to release lock: name={name}, token={token}" - + (f", reason={reason}" if reason else ""), - ) - - -class LockNotOwnedError(LockReleaseError): - """Lock Not Owned Error during Release. - - This error is raised when an attempt is made to release a lock that is not owned, respectively - the token is different or the lock is already expired. - """ - - def __init__(self, *, name: str, token: str) -> None: - """Initialize the error.""" - super().__init__(name=name, token=token, reason="lock not owned") - - -class SyncSettingsValidationError(SyncError, SettingsValidationError): - """Synchronization Settings Validation Error.""" diff --git a/.conflict-base-0/grelmicro/sync/leaderelection.py b/.conflict-base-0/grelmicro/sync/leaderelection.py deleted file mode 100644 index 62ce539..0000000 --- a/.conflict-base-0/grelmicro/sync/leaderelection.py +++ /dev/null @@ -1,386 +0,0 @@ -"""Leader Election.""" - -from logging import getLogger -from time import monotonic -from types import TracebackType -from typing import TYPE_CHECKING, Annotated, Self -from uuid import UUID, uuid1 - -from anyio import ( - TASK_STATUS_IGNORED, - CancelScope, - Condition, - fail_after, - get_cancelled_exc_class, - move_on_after, - sleep, -) -from anyio.abc import TaskStatus -from pydantic import BaseModel, model_validator -from typing_extensions import Doc - -from grelmicro.sync._backends import get_sync_backend -from grelmicro.sync.abc import Seconds, SyncBackend, Synchronization -from grelmicro.task.abc import Task - -if TYPE_CHECKING: - from contextlib import AsyncExitStack - - from anyio.abc import TaskGroup - -logger = getLogger("grelmicro.leader_election") - - -class LeaderElectionConfig(BaseModel): - """Leader Election Config. - - Leader election based on a leased reentrant distributed lock. - """ - - name: Annotated[ - str, - Doc( - """ - The leader election lock name. - """, - ), - ] - worker: Annotated[ - str | UUID, - Doc( - """ - The worker identity used as lock token. - """, - ), - ] - lease_duration: Annotated[ - Seconds, - Doc( - """ - The lease duration in seconds. - """, - ), - ] = 15 - renew_deadline: Annotated[ - Seconds, - Doc( - """ - The renew deadline in seconds. - """, - ), - ] = 10 - retry_interval: Annotated[ - Seconds, - Doc( - """ - The retry interval in seconds. - """, - ), - ] = 2 - backend_timeout: Annotated[ - Seconds, - Doc( - """ - The backend timeout in seconds. - """, - ), - ] = 5 - error_interval: Annotated[ - Seconds, - Doc( - """ - The error interval in seconds. - """, - ), - ] = 30 - - @model_validator(mode="after") - def _validate(self) -> Self: - if self.renew_deadline >= self.lease_duration: - msg = "Renew deadline must be shorter than lease duration" - raise ValueError(msg) - if self.retry_interval >= self.renew_deadline: - msg = "Retry interval must be shorter than renew deadline" - raise ValueError(msg) - if self.backend_timeout >= self.renew_deadline: - msg = "Backend timeout must be shorter than renew deadline" - raise ValueError(msg) - return self - - -class LeaderElection(Synchronization, Task): - """Leader Election. - - The leader election is a synchronization primitive with the worker as scope. - It runs as a task to acquire or renew the distributed lock. - """ - - def __init__( - self, - name: Annotated[ - str, - Doc( - """ - The name of the resource representing the leader election. - - It will be used as the lock name so make sure it is unique on the distributed lock - backend. - """, - ), - ], - *, - backend: Annotated[ - SyncBackend | None, - Doc( - """ - The distributed lock backend used to acquire and release the lock. - - By default, it will use the lock backend registry to get the default lock backend. - """, - ), - ] = None, - worker: Annotated[ - str | UUID | None, - Doc( - """ - The worker identity. - - By default, use a UUIDv1 will be generated. - """, - ), - ] = None, - lease_duration: Annotated[ - Seconds, - Doc( - """ - The duration in seconds after the lock will be released if not renewed. - - If the worker becomes unavailable, the lock can only be acquired by an other worker - after it' has expired. - """, - ), - ] = 15, - renew_deadline: Annotated[ - Seconds, - Doc( - """ - The duration in seconds that the leader worker will try to acquire the lock before - giving up. - - Must be shorter than the lease duration. In case of multiple failures, the leader - worker will loose the lead to prevent split-brain scenarios and ensure that only one - worker is the leader at any time. - """, - ), - ] = 10, - retry_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between attempts to acquire or renew the lock. - - Must be shorter than the renew deadline. A shorter schedule enables faster leader - elections but may increase load on the distributed lock backend, while a longer - schedule reduces load but can delay new leader elections. - """, - ), - ] = 2, - backend_timeout: Annotated[ - Seconds, - Doc( - """ - The duration in seconds for waiting on backend for acquiring and releasing the lock. - - This value determines how long the system will wait before giving up the current - operation. - """, - ), - ] = 5, - error_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between logging error messages. - - If shorter than the retry interval, it will log every error. It is used to prevent - flooding the logs when the lock backend is unavailable. - """, - ), - ] = 30, - ) -> None: - """Initialize the leader election.""" - self.config = LeaderElectionConfig( - name=name, - worker=worker or uuid1(), - lease_duration=lease_duration, - renew_deadline=renew_deadline, - retry_interval=retry_interval, - backend_timeout=backend_timeout, - error_interval=error_interval, - ) - self.backend = backend or get_sync_backend() - - self._service_running = False - self._state_change_condition: Condition = Condition() - self._is_leader: bool = False - self._state_updated_at: float = monotonic() - self._error_logged_at: float | None = None - self._task_group: TaskGroup | None = None - self._exit_stack: AsyncExitStack | None = None - - async def __aenter__(self) -> Self: - """Wait for the leader with the context manager.""" - await self.wait_for_leader() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the context manager.""" - - @property - def name(self) -> str: - """Return the task name.""" - return self.config.name - - def is_running(self) -> bool: - """Check if the leader election task is running.""" - return self._service_running - - def is_leader(self) -> bool: - """Check if the current worker is the leader. - - To avoid a split-brain scenario, the leader considers itself as no longer leader if the - renew deadline is reached. - - Returns: - True if the current worker is the leader, False otherwise. - - """ - if not self._is_leader: - return False - return not self._is_renew_deadline_reached() - - async def wait_for_leader(self) -> None: - """Wait until the current worker is the leader.""" - while not self.is_leader(): - async with self._state_change_condition: - await self._state_change_condition.wait() - - async def wait_lose_leader(self) -> None: - """Wait until the current worker is no longer the leader.""" - while self.is_leader(): - with move_on_after(self._seconds_before_expiration_deadline()): - async with self._state_change_condition: - await self._state_change_condition.wait() - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run polling loop service to acquire or renew the distributed lock.""" - task_status.started() - if self._service_running: - logger.warning("Leader Election already running: %s", self.name) - return - self._service_running = True - logger.info("Leader Election started: %s", self.name) - try: - while True: - await self._try_acquire_or_renew() - await sleep(self.config.retry_interval) - except get_cancelled_exc_class(): - logger.info("Leader Election stopped: %s", self.name) - raise - except BaseException: - logger.exception("Leader Election crashed: %s", self.name) - raise - finally: - self._service_running = False - with CancelScope(shield=True): - await self._release() - - async def _update_state( - self, *, is_leader: bool, raison_if_no_more_leader: str - ) -> None: - """Update the state of the leader election.""" - self._state_updated_at = monotonic() - if is_leader is self._is_leader: - return # No change - - self._is_leader = is_leader - - if is_leader: - logger.info("Leader Election acquired leadership: %s", self.name) - else: - logger.warning( - "Leader Election lost leadership: %s (%s)", - self.name, - raison_if_no_more_leader, - ) - - async with self._state_change_condition: - self._state_change_condition.notify_all() - - async def _try_acquire_or_renew(self) -> None: - """Try to acquire leadership.""" - try: - with fail_after(self.config.backend_timeout): - is_leader = await self.backend.acquire( - name=self.name, - token=str(self.config.worker), - duration=self.config.lease_duration, - ) - except Exception: - if self._check_error_interval(): - logger.exception( - "Leader Election failed to acquire lock: %s", self.name - ) - if self._is_renew_deadline_reached(): - await self._update_state( - is_leader=False, - raison_if_no_more_leader="renew deadline reached", - ) - else: - await self._update_state( - is_leader=is_leader, - raison_if_no_more_leader="lock not acquired", - ) - - def _seconds_before_expiration_deadline(self) -> float: - return max( - self._state_updated_at + self.config.lease_duration - monotonic(), 0 - ) - - def _check_error_interval(self) -> bool: - """Check if the cooldown interval allows to log the error.""" - is_logging_allowed = ( - not self._error_logged_at - or (monotonic() - self._error_logged_at) - > self.config.error_interval - ) - self._error_logged_at = monotonic() - return is_logging_allowed - - def _is_renew_deadline_reached(self) -> bool: - return ( - monotonic() - self._state_updated_at - ) >= self.config.renew_deadline - - async def _release(self) -> None: - try: - with fail_after(self.config.backend_timeout): - if not ( - await self.backend.release( - name=self.config.name, token=str(self.config.worker) - ) - ): - logger.info( - "Leader Election lock already released: %s", self.name - ) - except Exception: - logger.exception( - "Leader Election failed to release lock: %s", self.name - ) diff --git a/.conflict-base-0/grelmicro/sync/lock.py b/.conflict-base-0/grelmicro/sync/lock.py deleted file mode 100644 index c87d08f..0000000 --- a/.conflict-base-0/grelmicro/sync/lock.py +++ /dev/null @@ -1,324 +0,0 @@ -"""Grelmicro Lock.""" - -from time import sleep as thread_sleep -from types import TracebackType -from typing import Annotated, Self -from uuid import UUID, uuid1 - -from anyio import WouldBlock, from_thread, sleep -from typing_extensions import Doc - -from grelmicro.sync._backends import get_sync_backend -from grelmicro.sync._base import BaseLock, BaseLockConfig -from grelmicro.sync._utils import generate_task_token, generate_thread_token -from grelmicro.sync.abc import Seconds, SyncBackend -from grelmicro.sync.errors import ( - LockAcquireError, - LockNotOwnedError, - LockReleaseError, - SyncBackendError, -) - - -class LockConfig(BaseLockConfig, frozen=True, extra="forbid"): - """Lock Config.""" - - lease_duration: Annotated[ - Seconds, - Doc( - """ - The lease duration in seconds for the lock. - """, - ), - ] - retry_interval: Annotated[ - Seconds, - Doc( - """ - The interval in seconds between attempts to acquire the lock. - """, - ), - ] - - -class Lock(BaseLock): - """Lock. - - This lock is a distributed lock that is used to acquire a resource across multiple workers. The - lock is acquired asynchronously and can be extended multiple times manually. The lock is - automatically released after a duration if not extended. - """ - - def __init__( - self, - name: Annotated[ - str, - Doc( - """ - The name of the resource to lock. - - It will be used as the lock name so make sure it is unique on the lock backend. - """, - ), - ], - *, - backend: Annotated[ - SyncBackend | None, - Doc(""" - The distributed lock backend used to acquire and release the lock. - - By default, it will use the lock backend registry to get the default lock backend. - """), - ] = None, - worker: Annotated[ - str | UUID | None, - Doc( - """ - The worker identity. - - By default, use a UUIDv1 will be generated. - """, - ), - ] = None, - lease_duration: Annotated[ - Seconds, - Doc( - """ - The duration in seconds for the lock to be held by default. - """, - ), - ] = 60, - retry_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between attempts to acquire the lock. - - Should be greater or equal than 0.1 to prevent flooding the lock backend. - """, - ), - ] = 0.1, - ) -> None: - """Initialize the lock.""" - self._config: LockConfig = LockConfig( - name=name, - worker=worker or uuid1(), - lease_duration=lease_duration, - retry_interval=retry_interval, - ) - self.backend = backend or get_sync_backend() - self._from_thread: ThreadLockAdapter | None = None - - async def __aenter__(self) -> Self: - """Acquire the lock with the async context manager.""" - await self.acquire() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock with the async context manager. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - await self.release() - - @property - def config(self) -> LockConfig: - """Return the lock config.""" - return self._config - - @property - def from_thread(self) -> "ThreadLockAdapter": - """Return the lock adapter for worker thread.""" - if self._from_thread is None: - self._from_thread = ThreadLockAdapter(lock=self) - return self._from_thread - - async def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - while not await self.do_acquire(token=token): # noqa: ASYNC110 // Polling is intentional - await sleep(self._config.retry_interval) - - async def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - WouldBlock: If the lock cannot be acquired without blocking. - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - if not await self.do_acquire(token=token): - msg = f"Lock not acquired: name={self._config.name}, token={token}" - raise WouldBlock(msg) - - async def release(self) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - if not await self.do_release(token): - raise LockNotOwnedError(name=self._config.name, token=token) - - async def locked(self) -> bool: - """Check if the lock is acquired. - - Raise: - SyncBackendError: If the lock cannot be checked due to an error on the backend. - """ - try: - return await self.backend.locked(name=self._config.name) - except Exception as exc: - msg = "Failed to check if the lock is acquired" - raise SyncBackendError(msg) from exc - - async def owned(self) -> bool: - """Check if the lock is owned by the current token. - - Raise: - SyncBackendError: If the lock cannot be checked due to an error on the backend. - """ - return await self.do_owned(generate_task_token(self._config.worker)) - - async def do_acquire(self, token: str) -> bool: - """Acquire the lock. - - This method should not be called directly. Use `acquire` instead. - - Returns: - bool: True if the lock was acquired, False if the lock was not acquired. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - """ - try: - return await self.backend.acquire( - name=self._config.name, - token=token, - duration=self._config.lease_duration, - ) - except Exception as exc: - raise LockAcquireError(name=self._config.name, token=token) from exc - - async def do_release(self, token: str) -> bool: - """Release the lock. - - This method should not be called directly. Use `release` instead. - - Returns: - bool: True if the lock was released, False otherwise. - - Raises: - LockReleaseError: Cannot release the lock due to backend error. - """ - try: - return await self.backend.release( - name=self._config.name, token=token - ) - except Exception as exc: - raise LockReleaseError(name=self._config.name, token=token) from exc - - async def do_owned(self, token: str) -> bool: - """Check if the lock is owned by the current token. - - This method should not be called directly. Use `owned` instead. - - Returns: - bool: True if the lock is owned by the current token, False otherwise. - - Raises: - SyncBackendError: Cannot check if the lock is owned due to backend error. - """ - try: - return await self.backend.owned(name=self._config.name, token=token) - except Exception as exc: - msg = "Failed to check if the lock is owned" - raise SyncBackendError(msg) from exc - - -class ThreadLockAdapter: - """Lock Adapter for Worker Thread.""" - - def __init__(self, lock: Lock) -> None: - """Initialize the lock adapter.""" - self._lock = lock - - def __enter__(self) -> Self: - """Acquire the lock with the context manager.""" - self.acquire() - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock with the context manager.""" - self.release() - - def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: Cannot acquire the lock due to backend error. - - """ - token = generate_thread_token(self._lock.config.worker) - retry_interval = self._lock.config.retry_interval - while not from_thread.run(self._lock.do_acquire, token): - thread_sleep(retry_interval) - - def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - LockAcquireError: Cannot acquire the lock due to backend error. - WouldBlock: If the lock cannot be acquired without blocking. - - """ - token = generate_thread_token(self._lock.config.worker) - if not from_thread.run(self._lock.do_acquire, token): - msg = f"Lock not acquired: name={self._lock.config.name}, token={token}" - raise WouldBlock(msg) - - def release(self) -> None: - """Release the lock. - - Raises: - ReleaseSyncBackendError: Cannot release the lock due to backend error. - LockNotOwnedError: If the lock is not currently held. - - """ - token = generate_thread_token(self._lock.config.worker) - if not from_thread.run(self._lock.do_release, token): - raise LockNotOwnedError(name=self._lock.config.name, token=token) - - def locked(self) -> bool: - """Return True if the lock is currently held.""" - return from_thread.run(self._lock.locked) - - def owned(self) -> bool: - """Return True if the lock is currently held by the current worker thread.""" - return from_thread.run( - self._lock.do_owned, generate_thread_token(self._lock.config.worker) - ) diff --git a/.conflict-base-0/grelmicro/sync/memory.py b/.conflict-base-0/grelmicro/sync/memory.py deleted file mode 100644 index 9746c59..0000000 --- a/.conflict-base-0/grelmicro/sync/memory.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Memory Synchronization Backend.""" - -from time import monotonic -from types import TracebackType -from typing import Annotated, Self - -from typing_extensions import Doc - -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend - - -class MemorySyncBackend(SyncBackend): - """Memory Synchronization Backend. - - This is not a backend with a real distributed lock. It is a local lock that can be used for - testing purposes or for locking operations that are executed in the same AnyIO event loop. - """ - - def __init__( - self, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - ) -> None: - """Initialize the lock backend.""" - self._locks: dict[str, tuple[str | None, float]] = {} - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Enter the lock backend.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Exit the lock backend.""" - self._locks.clear() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - if ( - current_token is None - or current_token == token - or expire_at < monotonic() - ): - self._locks[name] = (token, monotonic() + duration) - return True - return False - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - if current_token == token and expire_at >= monotonic(): - del self._locks[name] - return True - if current_token and expire_at < monotonic(): - del self._locks[name] - return False - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - return current_token is not None and expire_at >= monotonic() - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - return current_token == token and expire_at >= monotonic() diff --git a/.conflict-base-0/grelmicro/sync/postgres.py b/.conflict-base-0/grelmicro/sync/postgres.py deleted file mode 100644 index 8614e89..0000000 --- a/.conflict-base-0/grelmicro/sync/postgres.py +++ /dev/null @@ -1,204 +0,0 @@ -"""PostgreSQL Synchronization Backend.""" - -from types import TracebackType -from typing import Annotated, Self - -from asyncpg import Pool, create_pool -from pydantic import PostgresDsn -from pydantic_core import MultiHostUrl, ValidationError -from pydantic_settings import BaseSettings -from typing_extensions import Doc - -from grelmicro.errors import OutOfContextError -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import SyncSettingsValidationError - - -class _PostgresSettings(BaseSettings): - POSTGRES_HOST: str | None = None - POSTGRES_PORT: int = 5432 - POSTGRES_DB: str | None = None - POSTGRES_USER: str | None = None - POSTGRES_PASSWORD: str | None = None - POSTGRES_URL: PostgresDsn | None = None - - -def _get_postgres_url() -> str: - """Get the PostgreSQL URL from the environment variables. - - Raises: - SyncSettingsValidationError: If the URL or all of the host, database, user, and password - """ - try: - settings = _PostgresSettings() - except ValidationError as error: - raise SyncSettingsValidationError(error) from None - - parts_fields = [ - settings.POSTGRES_HOST, - settings.POSTGRES_DB, - settings.POSTGRES_USER, - settings.POSTGRES_PASSWORD, - ] - - if settings.POSTGRES_URL and not any(parts_fields): - return settings.POSTGRES_URL.unicode_string() - - if all(parts_fields) and not settings.POSTGRES_URL: - return MultiHostUrl.build( - scheme="postgresql", - username=settings.POSTGRES_USER, - password=settings.POSTGRES_PASSWORD, - host=settings.POSTGRES_HOST, - port=settings.POSTGRES_PORT, - path=f"/{settings.POSTGRES_DB}", - ).unicode_string() - - msg = ( - "Either POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and " - "POSTGRES_PASSWORD must be set" - ) - raise SyncSettingsValidationError(msg) - - -class PostgresSyncBackend(SyncBackend): - """PostgreSQL Synchronization Backend.""" - - _SQL_CREATE_TABLE_IF_NOT_EXISTS = """ - CREATE TABLE IF NOT EXISTS {table_name} ( - name TEXT PRIMARY KEY, - token TEXT NOT NULL, - expire_at TIMESTAMP NOT NULL - ); - """ - - _SQL_ACQUIRE_OR_EXTEND = """ - INSERT INTO {table_name} (name, token, expire_at) - VALUES ($1, $2, NOW() + make_interval(secs => $3)) - ON CONFLICT (name) DO UPDATE - SET token = EXCLUDED.token, expire_at = EXCLUDED.expire_at - WHERE {table_name}.token = EXCLUDED.token OR {table_name}.expire_at < NOW() - RETURNING 1; - """ - - _SQL_RELEASE = """ - DELETE FROM {table_name} - WHERE name = $1 AND token = $2 AND expire_at >= NOW() - RETURNING 1; - """ - - _SQL_RELEASE_ALL_EXPIRED = """ - DELETE FROM {table_name} - WHERE expire_at < NOW(); - """ - - _SQL_LOCKED = """ - SELECT 1 FROM {table_name} - WHERE name = $1 AND expire_at >= NOW(); - """ - - _SQL_OWNED = """ - SELECT 1 FROM {table_name} - WHERE name = $1 AND token = $2 AND expire_at >= NOW(); - """ - - def __init__( - self, - url: Annotated[ - PostgresDsn | str | None, - Doc(""" - The Postgres database URL. - - If not provided, the URL will be taken from the environment variables POSTGRES_URL - or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD. - """), - ] = None, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - table_name: Annotated[ - str, Doc("The table name to store the locks.") - ] = "locks", - ) -> None: - """Initialize the lock backend.""" - if not table_name.isidentifier(): - msg = f"Table name '{table_name}' is not a valid identifier" - raise ValueError(msg) - - self._url = url or _get_postgres_url() - self._table_name = table_name - self._acquire_sql = self._SQL_ACQUIRE_OR_EXTEND.format( - table_name=table_name - ) - self._release_sql = self._SQL_RELEASE.format(table_name=table_name) - self._pool: Pool | None = None - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Enter the lock backend.""" - self._pool = await create_pool(str(self._url)) - await self._pool.execute( - self._SQL_CREATE_TABLE_IF_NOT_EXISTS.format( - table_name=self._table_name - ), - ) - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Exit the lock backend.""" - if self._pool: - await self._pool.execute( - self._SQL_RELEASE_ALL_EXPIRED.format( - table_name=self._table_name - ), - ) - await self._pool.close() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire a lock.""" - if not self._pool: - raise OutOfContextError(self, "acquire") - - return bool( - await self._pool.fetchval(self._acquire_sql, name, token, duration) - ) - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - if not self._pool: - raise OutOfContextError(self, "release") - return bool(await self._pool.fetchval(self._release_sql, name, token)) - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - if not self._pool: - raise OutOfContextError(self, "locked") - return bool( - await self._pool.fetchval( - self._SQL_LOCKED.format(table_name=self._table_name), - name, - ), - ) - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - if not self._pool: - raise OutOfContextError(self, "owned") - return bool( - await self._pool.fetchval( - self._SQL_OWNED.format(table_name=self._table_name), - name, - token, - ), - ) diff --git a/.conflict-base-0/grelmicro/sync/redis.py b/.conflict-base-0/grelmicro/sync/redis.py deleted file mode 100644 index 73090c8..0000000 --- a/.conflict-base-0/grelmicro/sync/redis.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Redis Synchronization Backend.""" - -from types import TracebackType -from typing import Annotated, Self - -from pydantic import RedisDsn, ValidationError -from pydantic_core import Url -from pydantic_settings import BaseSettings -from redis.asyncio.client import Redis -from typing_extensions import Doc - -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import SyncSettingsValidationError - - -class _RedisSettings(BaseSettings): - """Redis settings from the environment variables.""" - - REDIS_HOST: str | None = None - REDIS_PORT: int = 6379 - REDIS_DB: int = 0 - REDIS_PASSWORD: str | None = None - REDIS_URL: RedisDsn | None = None - - -def _get_redis_url() -> str: - """Get the Redis URL from the environment variables. - - Raises: - SyncSettingsValidationError: If the URL or host is not set. - """ - try: - settings = _RedisSettings() - except ValidationError as error: - raise SyncSettingsValidationError(error) from None - - if settings.REDIS_URL and not settings.REDIS_HOST: - return settings.REDIS_URL.unicode_string() - - if settings.REDIS_HOST and not settings.REDIS_URL: - return Url.build( - scheme="redis", - host=settings.REDIS_HOST, - port=settings.REDIS_PORT, - path=str(settings.REDIS_DB), - password=settings.REDIS_PASSWORD, - ).unicode_string() - - msg = "Either REDIS_URL or REDIS_HOST must be set" - raise SyncSettingsValidationError(msg) - - -class RedisSyncBackend(SyncBackend): - """Redis Synchronization Backend.""" - - _LUA_ACQUIRE_OR_EXTEND = """ - local token = redis.call('get', KEYS[1]) - if not token then - redis.call('set', KEYS[1], ARGV[1], 'px', ARGV[2]) - return 1 - end - if token == ARGV[1] then - redis.call('pexpire', KEYS[1], ARGV[2]) - return 1 - end - return 0 - """ - _LUA_RELEASE = """ - local token = redis.call('get', KEYS[1]) - if not token or token ~= ARGV[1] then - return 0 - end - redis.call('del', KEYS[1]) - return 1 - """ - - def __init__( - self, - url: Annotated[ - RedisDsn | str | None, - Doc(""" - The Redis URL. - - If not provided, the URL will be taken from the environment variables REDIS_URL - or REDIS_HOST, REDIS_PORT, REDIS_DB, and REDIS_PASSWORD. - """), - ] = None, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - ) -> None: - """Initialize the lock backend.""" - self._url = url or _get_redis_url() - self._redis: Redis = Redis.from_url(str(self._url)) - self._lua_release = self._redis.register_script(self._LUA_RELEASE) - self._lua_acquire = self._redis.register_script( - self._LUA_ACQUIRE_OR_EXTEND - ) - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Open the lock backend.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Close the lock backend.""" - await self._redis.aclose() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock.""" - return bool( - await self._lua_acquire( - keys=[name], - args=[token, int(duration * 1000)], - client=self._redis, - ) - ) - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - return bool( - await self._lua_release( - keys=[name], args=[token], client=self._redis - ) - ) - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - return bool(await self._redis.get(name)) - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - return bool( - (await self._redis.get(name)) == token.encode() - ) # redis returns bytes diff --git a/.conflict-base-0/grelmicro/task/__init__.py b/.conflict-base-0/grelmicro/task/__init__.py deleted file mode 100644 index 374bf08..0000000 --- a/.conflict-base-0/grelmicro/task/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Grelmicro Task Scheduler.""" - -from grelmicro.task.manager import TaskManager -from grelmicro.task.router import TaskRouter - -__all__ = ["TaskManager", "TaskRouter"] diff --git a/.conflict-base-0/grelmicro/task/_interval.py b/.conflict-base-0/grelmicro/task/_interval.py deleted file mode 100644 index f66c2f2..0000000 --- a/.conflict-base-0/grelmicro/task/_interval.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Interval Task.""" - -from collections.abc import Awaitable, Callable -from contextlib import nullcontext -from functools import partial -from inspect import iscoroutinefunction -from logging import getLogger -from typing import Any - -from anyio import TASK_STATUS_IGNORED, sleep, to_thread -from anyio.abc import TaskStatus -from fast_depends import inject - -from grelmicro.sync.abc import Synchronization -from grelmicro.task._utils import validate_and_generate_reference -from grelmicro.task.abc import Task - -logger = getLogger("grelmicro.task") - - -class IntervalTask(Task): - """Interval Task. - - Use the `TaskManager.interval()` or `SchedulerRouter.interval()` decorator instead - of creating IntervalTask objects directly. - """ - - def __init__( - self, - *, - function: Callable[..., Any], - name: str | None = None, - interval: float, - sync: Synchronization | None = None, - ) -> None: - """Initialize the IntervalTask. - - Raises: - FunctionNotSupportedError: If the function is not supported. - ValueError: If internal is less than or equal to 0. - """ - if interval <= 0: - msg = "Interval must be greater than 0" - raise ValueError(msg) - - alt_name = validate_and_generate_reference(function) - self._name = name or alt_name - self._interval = interval - self._async_function = self._prepare_async_function(function) - self._sync = sync if sync else nullcontext() - - @property - def name(self) -> str: - """Return the lock name.""" - return self._name - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run the repeated task loop.""" - logger.info( - "Task started (interval: %ss): %s", self._interval, self.name - ) - task_status.started() - try: - while True: - try: - async with self._sync: - try: - await self._async_function() - except Exception: - logger.exception( - "Task execution error: %s", self.name - ) - except Exception: - logger.exception( - "Task synchronization error: %s", self.name - ) - await sleep(self._interval) - finally: - logger.info("Task stopped: %s", self.name) - - def _prepare_async_function( - self, function: Callable[..., Any] - ) -> Callable[..., Awaitable[Any]]: - """Prepare the function with lock and ensure async function.""" - function = inject(function) - return ( - function - if iscoroutinefunction(function) - else partial(to_thread.run_sync, function) - ) diff --git a/.conflict-base-0/grelmicro/task/_utils.py b/.conflict-base-0/grelmicro/task/_utils.py deleted file mode 100644 index 7cfec3f..0000000 --- a/.conflict-base-0/grelmicro/task/_utils.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Task Utilities.""" - -from collections.abc import Callable -from functools import partial -from inspect import ismethod -from typing import Any - -from grelmicro.task.errors import FunctionTypeError - - -def validate_and_generate_reference(function: Callable[..., Any]) -> str: - """Generate a task name from the given function. - - This implementation is inspirated by the APScheduler project under MIT License. - Original source: https://github.com/agronholm/apscheduler/blob/master/src/apscheduler/_marshalling.py - - Raises: - FunctionNotSupportedError: If function is not supported. - - """ - if isinstance(function, partial): - ref = "partial()" - raise FunctionTypeError(ref) - - if ismethod(function): - ref = "method" - raise FunctionTypeError(ref) - - if not hasattr(function, "__module__") or not hasattr( - function, "__qualname__" - ): - ref = "callable without __module__ or __qualname__ attribute" - raise FunctionTypeError(ref) - - if "" in function.__qualname__: - ref = "lambda" - raise FunctionTypeError(ref) - - if "" in function.__qualname__: - ref = "nested function" - raise FunctionTypeError(ref) - - return f"{function.__module__}:{function.__qualname__}" diff --git a/.conflict-base-0/grelmicro/task/abc.py b/.conflict-base-0/grelmicro/task/abc.py deleted file mode 100644 index d4e7cf3..0000000 --- a/.conflict-base-0/grelmicro/task/abc.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Grelmicro Task Synchronization Abstract Base Classes and Protocols.""" - -from typing import Protocol - -from anyio import TASK_STATUS_IGNORED -from anyio.abc import TaskStatus -from typing_extensions import runtime_checkable - - -@runtime_checkable -class Task(Protocol): - """Task Protocol. - - A task that runs in background in the async event loop. - """ - - @property - def name(self) -> str: - """Name to uniquely identify the task.""" - ... - - async def __call__( - self, - *, - task_status: TaskStatus[None] = TASK_STATUS_IGNORED, - ) -> None: - """Run the task. - - This is the entry point of the task to be run in the async event loop. - """ - ... diff --git a/.conflict-base-0/grelmicro/task/errors.py b/.conflict-base-0/grelmicro/task/errors.py deleted file mode 100644 index a788f61..0000000 --- a/.conflict-base-0/grelmicro/task/errors.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Grelmicro Task Scheduler Errors.""" - -from grelmicro.errors import GrelmicroError - - -class TaskError(GrelmicroError): - """Base Grelmicro Task error.""" - - -class FunctionTypeError(TaskError, TypeError): - """Function Type Error.""" - - def __init__(self, reference: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not use function {reference}, " - "try declaring 'def' or 'async def' directly in the module" - ) - - -class TaskAddOperationError(TaskError, RuntimeError): - """Task Add Operation Error.""" - - def __init__(self) -> None: - """Initialize the error.""" - super().__init__( - "Could not add the task, try calling 'add_task' and 'include_router' before starting" - ) diff --git a/.conflict-base-0/grelmicro/task/manager.py b/.conflict-base-0/grelmicro/task/manager.py deleted file mode 100644 index 5432145..0000000 --- a/.conflict-base-0/grelmicro/task/manager.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Grelmicro Task Manager.""" - -from contextlib import AsyncExitStack -from logging import getLogger -from types import TracebackType -from typing import TYPE_CHECKING, Annotated, Self - -from anyio import create_task_group -from typing_extensions import Doc - -from grelmicro.errors import OutOfContextError -from grelmicro.task.abc import Task -from grelmicro.task.errors import TaskAddOperationError -from grelmicro.task.router import TaskRouter - -if TYPE_CHECKING: - from anyio.abc import TaskGroup - -logger = getLogger("grelmicro.task") - - -class TaskManager(TaskRouter): - """Task Manager. - - `TaskManager` class, the main entrypoint to manage scheduled tasks. - """ - - def __init__( - self, - *, - auto_start: Annotated[ - bool, - Doc( - """ - Automatically start all tasks. - """, - ), - ] = True, - tasks: Annotated[ - list[Task] | None, - Doc( - """ - A list of tasks to be started. - """, - ), - ] = None, - ) -> None: - """Initialize the task manager.""" - TaskRouter.__init__(self, tasks=tasks) - - self._auto_start = auto_start - self._task_group: TaskGroup | None = None - - async def __aenter__(self) -> Self: - """Enter the context manager.""" - self._exit_stack = AsyncExitStack() - await self._exit_stack.__aenter__() - self._task_group = await self._exit_stack.enter_async_context( - create_task_group(), - ) - if self._auto_start: - await self.start() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the context manager.""" - if not self._task_group or not self._exit_stack: - raise OutOfContextError(self, "__aexit__") - self._task_group.cancel_scope.cancel() - return await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb) - - async def start(self) -> None: - """Start all tasks manually.""" - if not self._task_group: - raise OutOfContextError(self, "start") - - if self._started: - raise TaskAddOperationError - - self.do_mark_as_started() - - for task in self.tasks: - await self._task_group.start(task.__call__) - logger.debug("%s scheduled tasks started", len(self._tasks)) diff --git a/.conflict-base-0/grelmicro/task/router.py b/.conflict-base-0/grelmicro/task/router.py deleted file mode 100644 index 16b240d..0000000 --- a/.conflict-base-0/grelmicro/task/router.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Grelmicro Task Router.""" - -from collections.abc import Awaitable, Callable -from typing import Annotated, Any - -from typing_extensions import Doc - -from grelmicro.sync.abc import Synchronization -from grelmicro.task.abc import Task -from grelmicro.task.errors import TaskAddOperationError - - -class TaskRouter: - """Task Router. - - `TaskRouter` class, used to group task schedules, for example to structure an app in - multiple files. It would then included in the `TaskManager`, or in another - `TaskRouter`. - """ - - def __init__( - self, - *, - tasks: Annotated[ - list[Task] | None, - Doc( - """ - A list of schedules or scheduled tasks to be scheduled. - """, - ), - ] = None, - ) -> None: - """Initialize the task router.""" - self._started = False - self._tasks: list[Task] = tasks or [] - self._routers: list[TaskRouter] = [] - - @property - def tasks(self) -> list[Task]: - """List of scheduled tasks.""" - return self._tasks + [ - task for router in self._routers for task in router.tasks - ] - - def add_task(self, task: Task) -> None: - """Add a task to the scheduler.""" - if self._started: - raise TaskAddOperationError - - self._tasks.append(task) - - def interval( - self, - *, - seconds: Annotated[ - float, - Doc( - """ - The duration in seconds between each task run. - - Accuracy is not guaranteed and may vary with system load. Consider the - execution time of the task when setting the interval. - """, - ), - ], - name: Annotated[ - str | None, - Doc( - """ - The name of the task. - - If None, a name will be generated automatically from the function. - """, - ), - ] = None, - sync: Annotated[ - Synchronization | None, - Doc( - """ - The synchronization primitive to use for the task. - - You can use a `LeasedLock` or a `LeaderElection`, for example. If None, - no synchronization is used and the task will run on all workers. - """, - ), - ] = None, - ) -> Callable[ - [Callable[..., Any | Awaitable[Any]]], - Callable[..., Any | Awaitable[Any]], - ]: - """Decorate function to add it to the task scheduler. - - Raises: - TaskNameGenerationError: If the task name generation fails. - """ - from grelmicro.task._interval import IntervalTask - - def decorator( - function: Callable[[], None | Awaitable[None]], - ) -> Callable[[], None | Awaitable[None]]: - self.add_task( - IntervalTask( - name=name, - function=function, - interval=seconds, - sync=sync, - ), - ) - return function - - return decorator - - def include_router(self, router: "TaskRouter") -> None: - """Include another router in this router.""" - if self._started: - raise TaskAddOperationError - - self._routers.append(router) - - def started(self) -> bool: - """Check if the task manager has started.""" - return self._started - - def do_mark_as_started(self) -> None: - """Mark the task manager as started. - - Do not call this method directly. It is called by the task manager when the task - manager is started. - """ - self._started = True - for router in self._routers: - router.do_mark_as_started() diff --git a/.conflict-base-0/mkdocs.yml b/.conflict-base-0/mkdocs.yml deleted file mode 100644 index 0b08e9f..0000000 --- a/.conflict-base-0/mkdocs.yml +++ /dev/null @@ -1,47 +0,0 @@ -site_name: Grelmicro -site_description: Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. -site_url: https://grelmicro.grel.info -theme: - name: material - palette: - primary: green - accent: light green - font: - text: 'Roboto' - code: 'Roboto Mono' - features: - - content.tabs.link - - content.code.copy - - content.code.select - - content.tooltips - - navigation.indexes - - navigation.instant - - navigation.instant.prefetch - - navigation.instant.progress - - navigation.top - - navigation.tracking - -repo_name: grelinfo/grelmicro -repo_url: https://github.com/grelinfo/grelmicro - -validation: - omitted_files: warn - absolute_links: warn - unrecognized_links: warn - -nav: -- Grelmicro: index.md -- User Guide: - - logging.md - - sync.md - - task.md - -markdown_extensions: - - admonition - - mdx_include: - base_path: docs - - pymdownx.highlight - - pymdownx.superfences - - pymdownx.inlinehilite - - pymdownx.tabbed: - alternate_style: true diff --git a/.conflict-base-0/pyproject.toml b/.conflict-base-0/pyproject.toml deleted file mode 100644 index 9bcca87..0000000 --- a/.conflict-base-0/pyproject.toml +++ /dev/null @@ -1,174 +0,0 @@ -[project] -name = "grelmicro" -description = "Grelmicro is a lightweight framework/toolkit for building async microservices in Python" -license = "MIT" -authors = [{ name = "Loïc Gremaud", email = "grelinfo@gmail.com"}] -readme = "README.md" - -classifiers = [ - "Intended Audience :: Information Technology", - "Intended Audience :: System Administrators", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python", - "Topic :: Internet", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development", - "Typing :: Typed", - "Development Status :: 1 - Planning", - "Environment :: Web Environment", - "Framework :: AsyncIO", - "Framework :: FastAPI", - "Framework :: Pydantic", - "Framework :: Pydantic :: 2", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", -] -dynamic = ["version"] - -requires-python = ">=3.11" - -dependencies = [ - "anyio>=4.0.0", - "pydantic>=2.5.0", - "fast-depends>=2.0.0", - "pydantic-settings>=2.5.0", -] - -[project.urls] - -Repository = "https://github.com/grelinfo/grelmicro.git" -Issues = "https://github.com/grelinfo/grelmicro/issues" - -[project.optional-dependencies] -standard = [ - "loguru>=0.7.2", - "orjson>=3.10.11", -] -postgres = [ - "asyncpg>=0.30.0", -] -redis = [ - "redis>=5.0.0", -] - -[dependency-groups] -dev = [ - "pytest-cov>=6.0.0", - "pytest>=8.0.0", - "mypy>=1.12.0", - "ruff>=0.7.4", - "testcontainers[postgres,redis]>=4.8.2", - "pytest-timeout>=2.3.1", - "pytest-mock>=3.14.0", - "pytest-randomly>=3.16.0", - "pre-commit>=4.0.1", - "fastapi>=0.115.5", - "fastapi-cli>=0.0.5", - "mdx-include>=1.4.2", - "faststream>=0.5.30", - "hatch>=1.13.0", -] -docs = [ - "mkdocs-material>=9.5.44", - "pygments>=2.18.0", - "pymdown-extensions>=10.12", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build] -skip-excluded-dirs = true -exclude = ["/tests", "/docs", "/examples"] - -[tool.hatch.version] -path = "grelmicro/__init__.py" - -[tool.ruff] -target-version = "py311" -line-length = 80 - -[tool.ruff.lint] -select = ["ALL"] -ignore = ["COM812", "ISC001"] # Ignore rules conflicting with the formatter. - -[tool.ruff.lint.extend-per-file-ignores] -"examples/*" = [ - "ARG001", - "ANN001", - "ANN201", - "D103", - "D100", - "INP001", - "T201", -] -"examples/logging/basic.py" = ["EM101", "TRY"] -"examples/task/router.py" = ["I001", "E402"] -"tests/*" = [ - "S101", - "SLF001" -] - -[tool.ruff.lint.pycodestyle] -max-line-length = 100 # reports only line that exceed 100 characters. - -[tool.ruff.lint.pydocstyle] -convention = "pep257" - -[tool.ruff.lint.pylint] -max-args = 10 - -[tool.mypy] -scripts_are_modules = true -plugins = [ - "pydantic.mypy" -] -follow_imports = "silent" -warn_redundant_casts = true -warn_unused_ignores = true -disallow_any_generics = true -check_untyped_defs = true -no_implicit_reexport = true -disallow_untyped_defs = true - -[[tool.mypy.overrides]] -module = ["asyncpg", "testcontainers.*"] -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = [ - "examples.*" -] -disallow_untyped_defs = false - - -[tool.pytest.ini_options] -addopts = """ - --cov=grelmicro - --cov-report term:skip-covered - --cov-report xml:cov.xml - --strict-config - --strict-markers - -m "not integration" -""" -markers = """ - integration: mark a test as an integration test (disabled by default). -""" - -testpaths = "tests" - -[tool.coverage.report] -sort = "-Cover" -exclude_also = [ - "if TYPE_CHECKING:", - "class .*\\bProtocol\\):", - "assert_never\\(.*\\)", -] diff --git a/.conflict-base-0/tests/__init__.py b/.conflict-base-0/tests/__init__.py deleted file mode 100644 index adc28b2..0000000 --- a/.conflict-base-0/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Tests.""" diff --git a/.conflict-base-0/tests/conftest.py b/.conflict-base-0/tests/conftest.py deleted file mode 100644 index 916c148..0000000 --- a/.conflict-base-0/tests/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Grelmicro Test Config.""" - -import pytest - - -@pytest.fixture -def anyio_backend() -> str: - """AnyIO Backend.""" - return "asyncio" diff --git a/.conflict-base-0/tests/logging/__init__.py b/.conflict-base-0/tests/logging/__init__.py deleted file mode 100644 index a1c677a..0000000 --- a/.conflict-base-0/tests/logging/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Logging Tests.""" diff --git a/.conflict-base-0/tests/logging/test_loguru.py b/.conflict-base-0/tests/logging/test_loguru.py deleted file mode 100644 index 9214250..0000000 --- a/.conflict-base-0/tests/logging/test_loguru.py +++ /dev/null @@ -1,274 +0,0 @@ -"""Test Logging Loguru.""" - -from collections.abc import Generator -from datetime import datetime -from io import StringIO - -import pytest -from loguru import logger -from pydantic import TypeAdapter - -from grelmicro.errors import DependencyNotFoundError -from grelmicro.logging.errors import LoggingSettingsValidationError -from grelmicro.logging.loguru import ( - JSON_FORMAT, - JSONRecordDict, - configure_logging, - json_formatter, - json_patcher, -) - -json_record_type_adapter = TypeAdapter(JSONRecordDict) - - -@pytest.fixture(autouse=True) -def cleanup_handlers() -> Generator[None, None, None]: - """Cleanup logging handlers.""" - logger.configure(handlers=[]) - yield - logger.remove() - - -def generate_logs() -> int: - """Generate logs.""" - logger.debug("Hello, World!") - logger.info("Hello, World!") - logger.warning("Hello, World!") - logger.error("Hello, Alice!", user="Alice") - try: - 1 / 0 # noqa: B018 - except ZeroDivisionError: - logger.exception("Hello, Bob!") - - return 5 - - -def assert_logs(logs: str) -> None: - """Assert logs.""" - ( - info, - warning, - error, - exception, - ) = ( - json_record_type_adapter.validate_json(line) - for line in logs.splitlines()[0:4] - ) - - expected_separator = 3 - - assert info["logger"] - assert info["logger"].startswith("tests.logging.test_loguru:generate_logs:") - assert len(info["logger"].split(":")) == expected_separator - assert info["time"] == datetime.fromisoformat(info["time"]).isoformat() - assert info["level"] == "INFO" - assert info["msg"] == "Hello, World!" - assert info["thread"] == "MainThread" - assert "ctx" not in info - - assert warning["logger"] - assert warning["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(warning["logger"].split(":")) == expected_separator - assert ( - warning["time"] == datetime.fromisoformat(warning["time"]).isoformat() - ) - assert warning["level"] == "WARNING" - assert warning["msg"] == "Hello, World!" - assert warning["thread"] == "MainThread" - assert "ctx" not in warning - - assert error["logger"] - assert error["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(error["logger"].split(":")) == expected_separator - assert error["time"] == datetime.fromisoformat(error["time"]).isoformat() - assert error["level"] == "ERROR" - assert error["msg"] == "Hello, Alice!" - assert error["thread"] == "MainThread" - assert error["ctx"] == {"user": "Alice"} - - assert exception["logger"] - assert exception["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(exception["logger"].split(":")) == expected_separator - assert ( - exception["time"] - == datetime.fromisoformat(exception["time"]).isoformat() - ) - assert exception["level"] == "ERROR" - assert exception["msg"] == "Hello, Bob!" - assert exception["thread"] == "MainThread" - assert exception["ctx"] == { - "exception": "ZeroDivisionError: division by zero", - } - - -def test_json_formatter() -> None: - """Test JSON Formatter.""" - # Arrange - sink = StringIO() - - # Act - logger.add(sink, format=json_formatter, level="INFO") - generate_logs() - - # Assert - assert_logs(sink.getvalue()) - - -def test_json_patching() -> None: - """Test JSON Patching.""" - # Arrange - sink = StringIO() - - # Act - # logger.patch(json_patcher) -> Patch is not working using logger.configure instead - logger.configure(patcher=json_patcher) - logger.add(sink, format=JSON_FORMAT, level="INFO") - generate_logs() - - # Assert - assert_logs(sink.getvalue()) - - -def test_configure_logging_default( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Default.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - configure_logging() - generate_logs() - - # Assert - assert_logs(capsys.readouterr().out) - - -def test_configure_logging_text( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Text.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "text") - - # Act - configure_logging() - generate_logs() - - # Assert - lines = capsys.readouterr().out.splitlines() - - assert "tests.logging.test_loguru:generate_logs:" in lines[0] - assert " | INFO | " in lines[0] - assert " - Hello, World!" in lines[0] - - assert "tests.logging.test_loguru:generate_logs:" in lines[1] - assert " | WARNING | " in lines[1] - assert " - Hello, World!" in lines[1] - - assert "tests.logging.test_loguru:generate_logs:" in lines[2] - assert " | ERROR | " in lines[2] - assert " - Hello, Alice!" in lines[2] - - assert "tests.logging.test_loguru:generate_logs:" in lines[3] - assert " | ERROR | " in lines[3] - assert " - Hello, Bob!" in lines[3] - assert "Traceback" in lines[4] - assert "ZeroDivisionError: division by zero" in lines[-1] - - -def test_configure_logging_json( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging JSON.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "json") - - # Act - configure_logging() - generate_logs() - - # Assert - assert_logs(capsys.readouterr().out) - - -def test_configure_logging_level( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Level.""" - # Arrange - monkeypatch.setenv("LOG_LEVEL", "DEBUG") - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - configure_logging() - logs_count = generate_logs() - - # Assert - assert len(capsys.readouterr().out.splitlines()) == logs_count - - -def test_configure_logging_invalid_level( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Invalid Level.""" - # Arrange - monkeypatch.setenv("LOG_LEVEL", "INVALID") - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - with pytest.raises( - LoggingSettingsValidationError, - match=( - r"Could not validate environment variables settings:\n" - r"- LOG_LEVEL: Input should be 'DEBUG', 'INFO', 'WARNING', 'ERROR' or 'CRITICAL'" - r" \[input=INVALID\]" - ), - ): - configure_logging() - - # Assert - assert not capsys.readouterr().out - - -def test_configure_logging_format_template( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Format Template.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "{level}: {message}") - - # Act - configure_logging() - generate_logs() - - # Assert - lines = capsys.readouterr().out.splitlines() - assert "INFO: Hello, World!" in lines[0] - assert "WARNING: Hello, World!" in lines[1] - assert "ERROR: Hello, Alice!" in lines[2] - assert "ERROR: Hello, Bob!" in lines[3] - assert "Traceback" in lines[4] - assert "ZeroDivisionError: division by zero" in lines[-1] - - -def test_configure_logging_dependency_not_found( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test Configure Logging Dependency Not Found.""" - # Arrange - monkeypatch.setattr("grelmicro.logging.loguru.loguru", None) - - # Act / Assert - with pytest.raises(DependencyNotFoundError, match="loguru"): - configure_logging() diff --git a/.conflict-base-0/tests/sync/__init__.py b/.conflict-base-0/tests/sync/__init__.py deleted file mode 100644 index 5e3b5c4..0000000 --- a/.conflict-base-0/tests/sync/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Synchronization Primitives Tests.""" diff --git a/.conflict-base-0/tests/sync/test_backends.py b/.conflict-base-0/tests/sync/test_backends.py deleted file mode 100644 index b08a92f..0000000 --- a/.conflict-base-0/tests/sync/test_backends.py +++ /dev/null @@ -1,370 +0,0 @@ -"""Test Synchronization Backends.""" - -from collections.abc import AsyncGenerator, Callable, Generator -from uuid import uuid4 - -import pytest -from anyio import sleep -from testcontainers.core.container import DockerContainer -from testcontainers.postgres import PostgresContainer -from testcontainers.redis import RedisContainer - -from grelmicro.sync._backends import get_sync_backend, loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import BackendNotLoadedError -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.sync.postgres import PostgresSyncBackend -from grelmicro.sync.redis import RedisSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(15)] - - -@pytest.fixture(scope="module") -def anyio_backend() -> str: - """AnyIO Backend Module Scope.""" - return "asyncio" - - -@pytest.fixture(scope="module") -def monkeypatch() -> Generator[pytest.MonkeyPatch, None, None]: - """Monkeypatch Module Scope.""" - monkeypatch = pytest.MonkeyPatch() - yield monkeypatch - monkeypatch.undo() - - -@pytest.fixture -def clean_registry() -> Generator[None, None, None]: - """Make sure the registry is clean.""" - loaded_backends.pop("lock", None) - yield - loaded_backends.pop("lock", None) - - -@pytest.fixture( - params=[ - "memory", - pytest.param("redis", marks=[pytest.mark.integration]), - pytest.param("postgres", marks=[pytest.mark.integration]), - ], - scope="module", -) -def backend_name(request: pytest.FixtureRequest) -> str: - """Backend Name.""" - return request.param - - -@pytest.fixture( - scope="module", -) -def container( - backend_name: str, - monkeypatch: pytest.MonkeyPatch, -) -> Generator[DockerContainer | None, None, None]: - """Test Container for each Backend.""" - if backend_name == "redis": - with RedisContainer() as container: - yield container - elif backend_name == "postgres": - monkeypatch.setenv("POSTGRES_HOST", "localhost") - monkeypatch.setenv("POSTGRES_PORT", "5432") - monkeypatch.setenv("POSTGRES_DB", "test") - monkeypatch.setenv("POSTGRES_USER", "test") - monkeypatch.setenv("POSTGRES_PASSWORD", "test") - with PostgresContainer() as container: - yield container - elif backend_name == "memory": - yield None - - -@pytest.fixture(scope="module") -async def backend( - backend_name: str, container: DockerContainer | None -) -> AsyncGenerator[SyncBackend]: - """Test Container for each Backend.""" - if backend_name == "redis" and container: - port = container.get_exposed_port(6379) - async with RedisSyncBackend(f"redis://localhost:{port}/0") as backend: - yield backend - elif backend_name == "postgres" and container: - port = container.get_exposed_port(5432) - async with PostgresSyncBackend( - f"postgresql://test:test@localhost:{port}/test" - ) as backend: - yield backend - elif backend_name == "memory": - async with MemorySyncBackend() as backend: - yield backend - - -async def test_acquire(backend: SyncBackend) -> None: - """Test acquire.""" - # Arrange - name = "test_acquire" - token = uuid4().hex - duration = 1 - - # Act - result = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result - - -async def test_acquire_reantrant(backend: SyncBackend) -> None: - """Test acquire is reantrant.""" - # Arrange - name = "test_acquire_reantrant" - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result1 - assert result2 - - -async def test_acquire_already_acquired(backend: SyncBackend) -> None: - """Test acquire when already acquired.""" - # Arrange - name = "test_acquire_already_acquired" - token1 = uuid4().hex - token2 = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token1, duration=duration) - result2 = await backend.acquire(name=name, token=token2, duration=duration) - - # Assert - assert token1 != token2 - assert result1 - assert not result2 - - -async def test_acquire_expired(backend: SyncBackend) -> None: - """Test acquire when expired.""" - # Arrange - name = "test_acquire_expired" - token = uuid4().hex - duration = 0.01 - - # Act - result = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result - assert result2 - - -async def test_acquire_already_acquired_expired(backend: SyncBackend) -> None: - """Test acquire when already acquired but expired.""" - # Arrange - name = "test_acquire_already_acquired_expired" + uuid4().hex - token1 = uuid4().hex - token2 = uuid4().hex - duration = 0.01 - - # Act - result = await backend.acquire(name=name, token=token1, duration=duration) - await sleep(duration * 2) - result2 = await backend.acquire(name=name, token=token2, duration=duration) - - # Assert - assert token1 != token2 - assert result - assert result2 - - -async def test_release_not_acquired(backend: SyncBackend) -> None: - """Test release when not acquired.""" - # Arrange - name = "test_release" + uuid4().hex - token = uuid4().hex - - # Act - result = await backend.release(name=name, token=token) - - # Assert - assert not result - - -async def test_release_acquired(backend: SyncBackend) -> None: - """Test release when acquired.""" - # Arrange - name = "test_release_acquired" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert result2 - - -async def test_release_not_reantrant(backend: SyncBackend) -> None: - """Test release is not reantrant.""" - # Arrange - name = "test_release_not_reantrant" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.release(name=name, token=token) - result3 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert result2 - assert not result3 - - -async def test_release_acquired_expired(backend: SyncBackend) -> None: - """Test release when acquired but expired.""" - # Arrange - name = "test_release_acquired_expired" + uuid4().hex - token = uuid4().hex - duration = 0.01 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert not result2 - - -async def test_release_not_acquired_expired(backend: SyncBackend) -> None: - """Test release when not acquired but expired.""" - # Arrange - name = "test_release_not_acquired_expired" + uuid4().hex - token = uuid4().hex - duration = 0.01 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert not result2 - - -async def test_locked(backend: SyncBackend) -> None: - """Test locked.""" - # Arrange - name = "test_locked" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - locked_before = await backend.locked(name=name) - await backend.acquire(name=name, token=token, duration=duration) - locked_after = await backend.locked(name=name) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_owned(backend: SyncBackend) -> None: - """Test owned.""" - # Arrange - name = "test_owned" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - owned_before = await backend.owned(name=name, token=token) - await backend.acquire(name=name, token=token, duration=duration) - owned_after = await backend.owned(name=name, token=token) - - # Assert - assert owned_before is False - assert owned_after is True - - -async def test_owned_another(backend: SyncBackend) -> None: - """Test owned another.""" - # Arrange - name = "test_owned_another" + uuid4().hex - token1 = uuid4().hex - token2 = uuid4().hex - duration = 1 - - # Act - owned_before = await backend.owned(name=name, token=token1) - await backend.acquire(name=name, token=token1, duration=duration) - owned_after = await backend.owned(name=name, token=token2) - - # Assert - assert owned_before is False - assert owned_after is False - - -@pytest.mark.parametrize( - "backend_factory", - [ - lambda: MemorySyncBackend(), - lambda: RedisSyncBackend("redis://localhost:6379/0"), - lambda: PostgresSyncBackend( - "postgresql://user:password@localhost:5432/db" - ), - ], -) -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend(backend_factory: Callable[[], SyncBackend]) -> None: - """Test Get Synchronization Backend.""" - # Arrange - expected_backend = backend_factory() - - # Act - backend = get_sync_backend() - - # Assert - assert backend is expected_backend - - -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend_not_loaded() -> None: - """Test Get Synchronization Backend Not Loaded.""" - # Act / Assert - with pytest.raises(BackendNotLoadedError): - get_sync_backend() - - -@pytest.mark.parametrize( - "backend_factory", - [ - lambda: MemorySyncBackend(auto_register=False), - lambda: RedisSyncBackend( - "redis://localhost:6379/0", auto_register=False - ), - lambda: PostgresSyncBackend( - "postgresql://user:password@localhost:5432/db", auto_register=False - ), - ], -) -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend_auto_register_disabled( - backend_factory: Callable[[], SyncBackend], -) -> None: - """Test Get Synchronization Backend.""" - # Arrange - backend_factory() - - # Act / Assert - with pytest.raises(BackendNotLoadedError): - get_sync_backend() diff --git a/.conflict-base-0/tests/sync/test_leaderelection.py b/.conflict-base-0/tests/sync/test_leaderelection.py deleted file mode 100644 index d357daa..0000000 --- a/.conflict-base-0/tests/sync/test_leaderelection.py +++ /dev/null @@ -1,457 +0,0 @@ -"""Test leader election.""" - -import math - -import pytest -from anyio import Event, create_task_group, sleep -from pydantic import ValidationError -from pytest_mock import MockerFixture - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.leaderelection import LeaderElection, LeaderElectionConfig -from grelmicro.sync.memory import MemorySyncBackend - -WORKERS = 4 -WORKER_1 = 0 -WORKER_2 = 1 -TEST_TIMEOUT = 1 - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(TEST_TIMEOUT)] - - -@pytest.fixture -def backend() -> SyncBackend: - """Return Memory Synchronization Backend.""" - return MemorySyncBackend() - - -@pytest.fixture -def configs() -> list[LeaderElectionConfig]: - """Leader election Config.""" - return [ - LeaderElectionConfig( - name="test_leader_election", - worker=f"worker_{i}", - lease_duration=0.02, - renew_deadline=0.015, - retry_interval=0.005, - error_interval=0.01, - backend_timeout=0.005, - ) - for i in range(WORKERS) - ] - - -@pytest.fixture -def leader_elections( - backend: SyncBackend, configs: list[LeaderElectionConfig] -) -> list[LeaderElection]: - """Leader elections.""" - return [ - LeaderElection(backend=backend, **configs[i].model_dump()) - for i in range(WORKERS) - ] - - -@pytest.fixture -def leader_election( - backend: SyncBackend, configs: list[LeaderElectionConfig] -) -> LeaderElection: - """Leader election.""" - return LeaderElection(backend=backend, **configs[WORKER_1].model_dump()) - - -async def wait_first_leader(leader_elections: list[LeaderElection]) -> None: - """Wait for the first leader to be elected.""" - - async def wrapper(leader_election: LeaderElection, event: Event) -> None: - """Wait for the leadership.""" - await leader_election.wait_for_leader() - event.set() - - async with create_task_group() as task_group: - event = Event() - for coroutine in leader_elections: - task_group.start_soon(wrapper, coroutine, event) - await event.wait() - task_group.cancel_scope.cancel() - - -def test_leader_election_config() -> None: - """Test leader election Config.""" - # Arrange - config = LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - lease_duration=0.01, - renew_deadline=0.008, - retry_interval=0.001, - error_interval=0.01, - backend_timeout=0.007, - ) - - # Assert - assert config.model_dump() == { - "name": "test_leader_election", - "worker": "worker_1", - "lease_duration": 0.01, - "renew_deadline": 0.008, - "retry_interval": 0.001, - "error_interval": 0.01, - "backend_timeout": 0.007, - } - - -def test_leader_election_config_defaults() -> None: - """Test leader election Config Defaults.""" - # Arrange - config = LeaderElectionConfig( - name="test_leader_election", worker="worker_1" - ) - - # Assert - assert config.model_dump() == { - "name": "test_leader_election", - "worker": "worker_1", - "lease_duration": 15, - "renew_deadline": 10, - "retry_interval": 2, - "error_interval": 30, - "backend_timeout": 5, - } - - -def test_leader_election_config_validation_errors() -> None: - """Test leader election Config Errors.""" - # Arrange - with pytest.raises( - ValidationError, - match="Renew deadline must be shorter than lease duration", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - lease_duration=15, - renew_deadline=20, - ) - with pytest.raises( - ValidationError, - match="Retry interval must be shorter than renew deadline", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - renew_deadline=10, - retry_interval=15, - ) - with pytest.raises( - ValidationError, - match="Backend timeout must be shorter than renew deadline", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - renew_deadline=10, - backend_timeout=15, - ) - - -async def test_lifecycle(leader_election: LeaderElection) -> None: - """Test leader election on worker complete lifecycle.""" - # Act - is_leader_before_start = leader_election.is_leader() - is_running_before_start = leader_election.is_running() - async with create_task_group() as tg: - await tg.start(leader_election) - is_running_after_start = leader_election.is_running() - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - tg.cancel_scope.cancel() - is_running_after_cancel = leader_election.is_running() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_cancel is False - - assert is_running_before_start is False - assert is_running_after_start is True - assert is_running_after_cancel is False - - -async def test_leader_election_context_manager( - leader_election: LeaderElection, -) -> None: - """Test leader election on worker using context manager.""" - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - async with leader_election: - is_leader_inside_context = leader_election.is_leader() - is_leader_after_context = leader_election.is_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_inside_context is True - assert is_leader_after_context is True - assert is_leader_after_cancel is False - - -async def test_leader_election_single_worker( - leader_election: LeaderElection, -) -> None: - """Test leader election on single worker.""" - # Act - async with create_task_group() as tg: - is_leader_before_start = leader_election.is_leader() - await tg.start(leader_election) - is_leader_inside_context = leader_election.is_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_inside_context is True - assert is_leader_after_cancel is False - - -async def test_leadership_abandon_on_renew_deadline_reached( - leader_election: LeaderElection, -) -> None: - """Test leader election abandons leadership when renew deadline is reached.""" - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - leader_election.config.retry_interval = math.inf - await leader_election.wait_lose_leader() - is_leader_after_not_renewed = leader_election.is_leader() - tg.cancel_scope.cancel() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_not_renewed is False - - -async def test_leadership_abandon_on_backend_failure( - leader_election: LeaderElection, - caplog: pytest.LogCaptureFixture, - mocker: MockerFixture, -) -> None: - """Test leader election abandons leadership when backend is unreachable.""" - # Arrange - caplog.set_level("WARNING") - - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - mocker.patch.object( - leader_election.backend, - "acquire", - side_effect=Exception("Backend Unreachable"), - ) - await leader_election.wait_lose_leader() - is_leader_after_not_renewed = leader_election.is_leader() - tg.cancel_scope.cancel() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_not_renewed is False - assert ( - "Leader Election lost leadership: test_leader_election (renew deadline reached)" - in caplog.messages - ) - - -async def test_unepexpected_stop( - leader_election: LeaderElection, mocker: MockerFixture -) -> None: - """Test leader election worker abandons leadership on unexpected stop.""" - - # Arrange - async def leader_election_unexpected_exception() -> None: - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - mock = mocker.patch.object( - leader_election, - "_try_acquire_or_renew", - side_effect=Exception("Unexpected Exception"), - ) - await leader_election.wait_lose_leader() - mock.reset_mock() - tg.cancel_scope.cancel() - - # Act / Assert - with pytest.raises(ExceptionGroup): - await leader_election_unexpected_exception() - - -async def test_release_on_cancel( - backend: SyncBackend, leader_election: LeaderElection, mocker: MockerFixture -) -> None: - """Test leader election on worker that releases the lock on cancel.""" - # Arrange - spy_release = mocker.spy(backend, "release") - - # Act - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - - # Assert - spy_release.assert_called_once() - - -async def test_release_failure_ignored( - backend: SyncBackend, - leader_election: LeaderElection, - mocker: MockerFixture, -) -> None: - """Test leader election on worker that ignores release failure.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Unreachable") - ) - - # Act - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - - -async def test_only_one_leader(leader_elections: list[LeaderElection]) -> None: - """Test leader election on multiple workers ensuring only one leader is elected.""" - # Act - leaders_before_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - async with create_task_group() as tg: - for leader_election in leader_elections: - await tg.start(leader_election) - await wait_first_leader(leader_elections) - leaders_after_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - tg.cancel_scope.cancel() - for leader_election in leader_elections: - await leader_election.wait_lose_leader() - leaders_after_cancel = [ - leader_election.is_leader() for leader_election in leader_elections - ] - - # Assert - assert sum(leaders_before_start) == 0 - assert sum(leaders_after_start) == 1 - assert sum(leaders_after_cancel) == 0 - - -async def test_leader_transition( - leader_elections: list[LeaderElection], -) -> None: - """Test leader election leader transition to another worker.""" - # Arrange - leaders_after_leader_election1_start = [False] * len(leader_elections) - leaders_after_all_start = [False] * len(leader_elections) - leaders_after_leader_election1_down = [False] * len(leader_elections) - - # Act - leaders_before_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - async with create_task_group() as workers_tg: - async with create_task_group() as worker1_tg: - await worker1_tg.start(leader_elections[WORKER_1]) - await leader_elections[WORKER_1].wait_for_leader() - leaders_after_leader_election1_start = [ - leader_election.is_leader() - for leader_election in leader_elections - ] - - for leader_election in leader_elections: - await workers_tg.start(leader_election) - leaders_after_all_start = [ - leader_election.is_leader() - for leader_election in leader_elections - ] - worker1_tg.cancel_scope.cancel() - - await leader_elections[WORKER_1].wait_lose_leader() - - await wait_first_leader(leader_elections) - leaders_after_leader_election1_down = [ - leader_election.is_leader() for leader_election in leader_elections - ] - workers_tg.cancel_scope.cancel() - - for leader_election in leader_elections[WORKER_2:]: - await leader_election.wait_lose_leader() - leaders_after_all_down = [ - leader_election.is_leader() for leader_election in leader_elections - ] - - # Assert - assert sum(leaders_before_start) == 0 - assert sum(leaders_after_leader_election1_start) == 1 - assert sum(leaders_after_all_start) == 1 - assert sum(leaders_after_leader_election1_down) == 1 - assert sum(leaders_after_all_down) == 0 - - assert leaders_after_leader_election1_start[WORKER_1] is True - assert leaders_after_leader_election1_down[WORKER_1] is False - - -async def test_error_interval( - backend: SyncBackend, - leader_elections: list[LeaderElection], - caplog: pytest.LogCaptureFixture, - mocker: MockerFixture, -) -> None: - """Test leader election on worker with error cooldown.""" - # Arrange - caplog.set_level("ERROR") - leader_elections[WORKER_1].config.error_interval = 1 - leader_elections[WORKER_2].config.error_interval = 0.001 - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Unreachable") - ) - - # Act - async with create_task_group() as tg: - await tg.start(leader_elections[WORKER_1]) - await sleep(0.01) - tg.cancel_scope.cancel() - leader_election1_nb_errors = sum( - 1 for record in caplog.records if record.levelname == "ERROR" - ) - caplog.clear() - - async with create_task_group() as tg: - await tg.start(leader_elections[WORKER_2]) - await sleep(0.01) - tg.cancel_scope.cancel() - leader_election2_nb_errors = sum( - 1 for record in caplog.records if record.levelname == "ERROR" - ) - - # Assert - assert leader_election1_nb_errors == 1 - assert leader_election2_nb_errors >= 1 diff --git a/.conflict-base-0/tests/sync/test_lock.py b/.conflict-base-0/tests/sync/test_lock.py deleted file mode 100644 index 42e0b04..0000000 --- a/.conflict-base-0/tests/sync/test_lock.py +++ /dev/null @@ -1,506 +0,0 @@ -"""Test Lock.""" - -import time -from collections.abc import AsyncGenerator - -import pytest -from anyio import WouldBlock, sleep, to_thread -from pytest_mock import MockerFixture - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import ( - LockAcquireError, - LockNotOwnedError, - LockReleaseError, - SyncBackendError, -) -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -WORKER_1 = 0 -WORKER_2 = 1 -WORKER_COUNT = 2 - -LOCK_NAME = "test_leased_lock" - - -@pytest.fixture -async def backend() -> AsyncGenerator[SyncBackend]: - """Return Memory Synchronization Backend.""" - async with MemorySyncBackend() as backend: - yield backend - - -@pytest.fixture -def locks(backend: SyncBackend) -> list[Lock]: - """Locks of multiple workers.""" - return [ - Lock( - backend=backend, - name=LOCK_NAME, - worker=f"worker_{i}", - lease_duration=0.01, - retry_interval=0.001, - ) - for i in range(WORKER_COUNT) - ] - - -@pytest.fixture -def lock(locks: list[Lock]) -> Lock: - """Lock.""" - return locks[WORKER_1] - - -async def test_lock_owned(locks: list[Lock]) -> None: - """Test Lock owned.""" - # Act - worker_1_owned_before = await locks[WORKER_1].owned() - worker_2_owned_before = await locks[WORKER_2].owned() - await locks[WORKER_1].acquire() - worker_1_owned_after = await locks[WORKER_1].owned() - worker_2_owned_after = await locks[WORKER_2].owned() - - # Assert - assert worker_1_owned_before is False - assert worker_2_owned_before is False - assert worker_1_owned_after is True - assert worker_2_owned_after is False - - -async def test_lock_from_thread_owned(locks: list[Lock]) -> None: - """Test Lock from thread owned.""" - # Arrange - worker_1_owned_before = None - worker_2_owned_before = None - worker_1_owned_after = None - worker_2_owned_after = None - - # Act - def sync() -> None: - nonlocal worker_1_owned_before - nonlocal worker_2_owned_before - nonlocal worker_1_owned_after - nonlocal worker_2_owned_after - - worker_1_owned_before = locks[WORKER_1].from_thread.owned() - worker_2_owned_before = locks[WORKER_2].from_thread.owned() - locks[WORKER_1].from_thread.acquire() - worker_1_owned_after = locks[WORKER_1].from_thread.owned() - worker_2_owned_after = locks[WORKER_2].from_thread.owned() - - await to_thread.run_sync(sync) - - # Assert - assert worker_1_owned_before is False - assert worker_2_owned_before is False - assert worker_1_owned_after is True - assert worker_2_owned_after is False - - -async def test_lock_context_manager(lock: Lock) -> None: - """Test Lock context manager.""" - # Act - locked_before = await lock.locked() - async with lock: - locked_inside = await lock.locked() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_inside is True - assert locked_after is False - - -async def test_lock_from_thread_context_manager_acquire(lock: Lock) -> None: - """Test Lock from thread context manager.""" - # Arrange - locked_before = None - locked_inside = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_inside - nonlocal locked_after - - locked_before = lock.from_thread.locked() - with lock.from_thread: - locked_inside = lock.from_thread.locked() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_inside is True - assert locked_after is False - - -async def test_lock_context_manager_wait(lock: Lock, locks: list[Lock]) -> None: - """Test Lock context manager wait.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act - locked_before = await lock.locked() - async with locks[WORKER_2]: # Wait until lock expires - locked_inside = await lock.locked() - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_inside is True - assert locked_after is False - - -async def test_lock_from_thread_context_manager_wait( - lock: Lock, locks: list[Lock] -) -> None: - """Test Lock from thread context manager wait.""" - # Arrange - locked_before = None - locked_inside = None - locked_after = None - await locks[WORKER_1].acquire() - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_inside - nonlocal locked_after - - locked_before = lock.from_thread.locked() - with locks[WORKER_2].from_thread: - locked_inside = lock.from_thread.locked() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is True - assert locked_inside is True - assert locked_after is False - - -async def test_lock_acquire(lock: Lock) -> None: - """Test Lock acquire.""" - # Act - locked_before = await lock.locked() - await lock.acquire() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_from_thread_acquire(lock: Lock) -> None: - """Test Lock from thread acquire.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_wait(lock: Lock, locks: list[Lock]) -> None: - """Test Lock acquire wait.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act - locked_before = await lock.locked() - await locks[WORKER_2].acquire() # Wait until lock expires - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_after is True - - -async def test_lock_from_thread_acquire_wait(lock: Lock) -> None: - """Test Lock from thread acquire wait.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_nowait(lock: Lock) -> None: - """Test Lock wait acquire.""" - # Act - locked_before = await lock.locked() - await lock.acquire_nowait() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_from_thread_acquire_nowait(lock: Lock) -> None: - """Test Lock from thread wait acquire.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire_nowait() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_nowait_would_block(locks: list[Lock]) -> None: - """Test Lock wait acquire would block.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act / Assert - with pytest.raises(WouldBlock): - await locks[WORKER_2].acquire_nowait() - - -async def test_lock_from_thread_acquire_nowait_would_block( - locks: list[Lock], -) -> None: - """Test Lock from thread wait acquire would block.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act / Assert - def sync() -> None: - with pytest.raises(WouldBlock): - locks[WORKER_2].from_thread.acquire_nowait() - - await to_thread.run_sync(sync) - - -async def test_lock_release(lock: Lock) -> None: - """Test Lock release.""" - # Act / Assert - with pytest.raises(LockNotOwnedError): - await lock.release() - - -async def test_lock_from_thread_release(lock: Lock) -> None: - """Test Lock from thread release.""" - - # Act / Assert - def sync() -> None: - with pytest.raises(LockNotOwnedError): - lock.from_thread.release() - - await to_thread.run_sync(sync) - - -async def test_lock_release_acquired(lock: Lock) -> None: - """Test Lock release acquired.""" - # Arrange - await lock.acquire() - - # Act - locked_before = await lock.locked() - await lock.release() - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_after is False - - -async def test_lock_from_thread_release_acquired(lock: Lock) -> None: - """Test Lock from thread release acquired.""" - # Arrange - locked_before = None - locked_after = None - - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - lock.from_thread.acquire() - - # Act - locked_before = lock.from_thread.locked() - lock.from_thread.release() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is True - assert locked_after is False - - -async def test_lock_release_expired(locks: list[Lock]) -> None: - """Test Lock release expired.""" - # Arrange - await locks[WORKER_1].acquire() - await sleep(locks[WORKER_1].config.lease_duration) - - # Act - worker_1_locked_before = await locks[WORKER_1].locked() - with pytest.raises(LockNotOwnedError): - await locks[WORKER_2].release() - - # Assert - assert worker_1_locked_before is False - - -async def test_lock_from_thread_release_expired(locks: list[Lock]) -> None: - """Test Lock from thread release expired.""" - # Arrange - worker_1_locked_before = None - - def sync() -> None: - nonlocal worker_1_locked_before - - locks[WORKER_1].from_thread.acquire() - time.sleep(locks[WORKER_1].config.lease_duration) - - # Act - worker_1_locked_before = locks[WORKER_1].from_thread.locked() - with pytest.raises(LockNotOwnedError): - locks[WORKER_2].from_thread.release() - - await to_thread.run_sync(sync) - - # Assert - assert worker_1_locked_before is False - - -async def test_lock_acquire_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock acquire backend error.""" - # Arrange - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Error") - ) - - # Act - with pytest.raises(LockAcquireError): - await lock.acquire() - - -async def test_lock_from_thread_acquire_backend_error( - backend: SyncBackend, - lock: Lock, - mocker: MockerFixture, -) -> None: - """Test Lock from thread acquire backend error.""" - # Arrange - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Error") - ) - - # Act - def sync() -> None: - with pytest.raises(LockAcquireError): - lock.from_thread.acquire() - - await to_thread.run_sync(sync) - - -async def test_lock_release_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock release backend error.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Error") - ) - - # Act - await lock.acquire() - with pytest.raises(LockReleaseError): - await lock.release() - - -async def test_lock_from_thread_release_backend_error( - backend: SyncBackend, - lock: Lock, - mocker: MockerFixture, -) -> None: - """Test Lock from thread release backend error.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Error") - ) - - # Act - def sync() -> None: - lock.from_thread.acquire() - with pytest.raises(LockReleaseError): - lock.from_thread.release() - - await to_thread.run_sync(sync) - - -async def test_lock_owned_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock owned backend error.""" - # Arrange - mocker.patch.object( - backend, "owned", side_effect=Exception("Backend Error") - ) - - # Act / Assert - with pytest.raises(SyncBackendError): - await lock.owned() - - -async def test_lock_locked_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock locked backend error.""" - # Arrange - mocker.patch.object( - backend, "locked", side_effect=Exception("Backend Error") - ) - - # Act / Assert - with pytest.raises(SyncBackendError): - await lock.locked() diff --git a/.conflict-base-0/tests/sync/test_postgres.py b/.conflict-base-0/tests/sync/test_postgres.py deleted file mode 100644 index ef8dd18..0000000 --- a/.conflict-base-0/tests/sync/test_postgres.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Tests for PostgreSQL Backends.""" - -import pytest - -from grelmicro.errors import OutOfContextError -from grelmicro.sync.errors import SyncSettingsValidationError -from grelmicro.sync.postgres import PostgresSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -URL = "postgres://user:password@localhost:5432/db" - - -@pytest.mark.parametrize( - "table_name", - [ - "locks table", - "%locks", - "locks;table", - "locks' OR '1'='1", - "locks; DROP TABLE users; --", - ], -) -def test_sync_backend_table_name_invalid(table_name: str) -> None: - """Test Synchronization Backend Table Name Invalid.""" - # Act / Assert - with pytest.raises( - ValueError, match="Table name '.*' is not a valid identifier" - ): - PostgresSyncBackend(url=URL, table_name=table_name) - - -async def test_sync_backend_out_of_context_errors() -> None: - """Test Synchronization Backend Out Of Context Errors.""" - # Arrange - backend = PostgresSyncBackend(url=URL) - name = "lock" - key = "token" - - # Act / Assert - with pytest.raises(OutOfContextError): - await backend.acquire(name=name, token=key, duration=1) - with pytest.raises(OutOfContextError): - await backend.release(name=name, token=key) - with pytest.raises(OutOfContextError): - await backend.locked(name=name) - with pytest.raises(OutOfContextError): - await backend.owned(name=name, token=key) - - -@pytest.mark.parametrize( - ("environs"), - [ - { - "POSTGRES_URL": "postgresql://test_user:test_password@test_host:1234/test_db" - }, - { - "POSTGRES_USER": "test_user", - "POSTGRES_PASSWORD": "test_password", - "POSTGRES_HOST": "test_host", - "POSTGRES_PORT": "1234", - "POSTGRES_DB": "test_db", - }, - ], -) -def test_postgres_env_var_settings( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test PostgreSQL Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Act - backend = PostgresSyncBackend() - - # Assert - assert ( - backend._url - == "postgresql://test_user:test_password@test_host:1234/test_db" - ) - - -@pytest.mark.parametrize( - ("environs"), - [ - { - "POSTGRES_URL": "test://test_user:test_password@test_host:1234/test_db" - }, - {"POSTGRES_USER": "test_user"}, - ], -) -def test_postgres_env_var_settings_validation_error( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test PostgreSQL Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Assert / Act - with pytest.raises( - SyncSettingsValidationError, - match=(r"Could not validate environment variables settings:\n"), - ): - PostgresSyncBackend() diff --git a/.conflict-base-0/tests/sync/test_redis.py b/.conflict-base-0/tests/sync/test_redis.py deleted file mode 100644 index a14bad7..0000000 --- a/.conflict-base-0/tests/sync/test_redis.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Tests for Redis Backends.""" - -import pytest - -from grelmicro.sync.errors import SyncSettingsValidationError -from grelmicro.sync.redis import RedisSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -URL = "redis://:test_password@test_host:1234/0" - - -@pytest.mark.parametrize( - ("environs"), - [ - {"REDIS_URL": URL}, - { - "REDIS_PASSWORD": "test_password", - "REDIS_HOST": "test_host", - "REDIS_PORT": "1234", - "REDIS_DB": "0", - }, - ], -) -def test_redis_env_var_settings( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Redis Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Act - backend = RedisSyncBackend() - - # Assert - assert backend._url == URL - - -@pytest.mark.parametrize( - ("environs"), - [ - {"REDIS_URL": "test://:test_password@test_host:1234/0"}, - {"REDIS_PASSWORD": "test_password"}, - { - "REDIS_URL": "test://:test_password@test_host:1234/0", - "REDIS_PASSWORD": "test_password", - "REDIS_HOST": "test_host", - "REDIS_PORT": "1234", - "REDIS_DB": "0", - }, - ], -) -def test_redis_env_var_settings_validation_error( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Redis Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Assert / Act - with pytest.raises( - SyncSettingsValidationError, - match=(r"Could not validate environment variables settings:\n"), - ): - RedisSyncBackend() diff --git a/.conflict-base-0/tests/sync/utils.py b/.conflict-base-0/tests/sync/utils.py deleted file mode 100644 index e20356b..0000000 --- a/.conflict-base-0/tests/sync/utils.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Test utilities for Lock.""" - -from anyio import Event, create_task_group, fail_after - -from grelmicro.sync._base import BaseLock - - -async def wait_first_acquired(locks: list[BaseLock]) -> None: - """Wait for the first lock to be acquired.""" - - async def wrapper(lock: BaseLock, event: Event) -> None: - """Send event when lock is acquired.""" - with fail_after(1): - await lock.acquire() - event.set() - - with fail_after(1): - async with create_task_group() as task_group: - event = Event() - for lock in locks: - task_group.start_soon(wrapper, lock, event) - await event.wait() - task_group.cancel_scope.cancel() diff --git a/.conflict-base-0/tests/task/__init__.py b/.conflict-base-0/tests/task/__init__.py deleted file mode 100644 index ebf85b3..0000000 --- a/.conflict-base-0/tests/task/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Task Scheduler Tests.""" diff --git a/.conflict-base-0/tests/task/samples.py b/.conflict-base-0/tests/task/samples.py deleted file mode 100644 index d19c153..0000000 --- a/.conflict-base-0/tests/task/samples.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Test Samples for the Task Component.""" - -from types import TracebackType -from typing import Self - -from anyio import TASK_STATUS_IGNORED, Condition, Event -from anyio.abc import TaskStatus -from typer import echo - -from grelmicro.sync.abc import Synchronization -from grelmicro.task.abc import Task - -condition = Condition() - - -def test1() -> None: - """Test Function.""" - echo("test1") - - -def test2() -> None: - """Test Function.""" - - -def test3(test: str = "test") -> None: - """Test Function.""" - - -async def notify() -> None: - """Test Function that notifies the condition.""" - async with condition: - condition.notify() - - -async def always_fail() -> None: - """Test Function that always fails.""" - msg = "Test Error" - raise ValueError(msg) - - -class SimpleClass: - """Test Class.""" - - def method(self) -> None: - """Test Method.""" - - @staticmethod - def static_method() -> None: - """Test Static Method.""" - - -class EventTask(Task): - """Test Scheduled Task with Event.""" - - def __init__(self, *, event: Event | None = None) -> None: - """Initialize the event task.""" - self._event = event or Event() - - @property - def name(self) -> str: - """Return the task name.""" - return "event_task" - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run the task that sets the event.""" - task_status.started() - self._event.set() - - -class BadLock(Synchronization): - """Bad Lock.""" - - async def __aenter__(self) -> Self: - """Enter the synchronization primitive.""" - msg = "Bad Lock" - raise ValueError(msg) - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the synchronization primitive.""" diff --git a/.conflict-base-0/tests/task/test_interval.py b/.conflict-base-0/tests/task/test_interval.py deleted file mode 100644 index 308d456..0000000 --- a/.conflict-base-0/tests/task/test_interval.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Test Interval Task.""" - -import pytest -from anyio import create_task_group, sleep, sleep_forever -from pytest_mock import MockFixture - -from grelmicro.task._interval import IntervalTask -from tests.task.samples import ( - BadLock, - always_fail, - condition, - notify, - test1, -) - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] - -INTERVAL = 0.1 -SLEEP = 0.01 - - -def test_interval_task_init() -> None: - """Test Interval Task Initialization.""" - # Act - task = IntervalTask(interval=1, function=test1) - # Assert - assert task.name == "tests.task.samples:test1" - - -def test_interval_task_init_with_name() -> None: - """Test Interval Task Initialization with Name.""" - # Act - task = IntervalTask(interval=1, function=test1, name="test1") - # Assert - assert task.name == "test1" - - -def test_interval_task_init_with_invalid_interval() -> None: - """Test Interval Task Initialization with Invalid Interval.""" - # Act / Assert - with pytest.raises(ValueError, match="Interval must be greater than 0"): - IntervalTask(interval=0, function=test1) - - -async def test_interval_task_start() -> None: - """Test Interval Task Start.""" - # Arrange - task = IntervalTask(interval=1, function=notify) - # Act - async with create_task_group() as tg: - await tg.start(task) - async with condition: - await condition.wait() - tg.cancel_scope.cancel() - - -async def test_interval_task_execution_error( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test Interval Task Execution Error.""" - # Arrange - task = IntervalTask(interval=1, function=always_fail) - # Act - async with create_task_group() as tg: - await tg.start(task) - await sleep(SLEEP) - tg.cancel_scope.cancel() - - # Assert - assert any( - "Task execution error:" in record.message - for record in caplog.records - if record.levelname == "ERROR" - ) - - -async def test_interval_task_synchronization_error( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test Interval Task Synchronization Error.""" - # Arrange - task = IntervalTask(interval=1, function=notify, sync=BadLock()) - - # Act - async with create_task_group() as tg: - await tg.start(task) - await sleep(SLEEP) - tg.cancel_scope.cancel() - - # Assert - assert any( - "Task synchronization error:" in record.message - for record in caplog.records - if record.levelname == "ERROR" - ) - - -async def test_interval_stop( - caplog: pytest.LogCaptureFixture, mocker: MockFixture -) -> None: - """Test Interval Task stop.""" - # Arrange - caplog.set_level("INFO") - - class CustomBaseException(BaseException): - pass - - mocker.patch( - "grelmicro.task._interval.sleep", side_effect=CustomBaseException - ) - task = IntervalTask(interval=1, function=test1) - - async def leader_election_during_runtime_error() -> None: - async with create_task_group() as tg: - await tg.start(task) - await sleep_forever() - - # Act - with pytest.raises(BaseExceptionGroup): - await leader_election_during_runtime_error() - - # Assert - assert any( - "Task stopped:" in record.message - for record in caplog.records - if record.levelname == "INFO" - ) diff --git a/.conflict-base-0/tests/task/test_manager.py b/.conflict-base-0/tests/task/test_manager.py deleted file mode 100644 index 62c9859..0000000 --- a/.conflict-base-0/tests/task/test_manager.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Test Task Manager.""" - -import pytest -from anyio import Event - -from grelmicro.errors import OutOfContextError -from grelmicro.task import TaskManager -from grelmicro.task.errors import TaskAddOperationError -from tests.task.samples import EventTask - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] - - -def test_task_manager_init() -> None: - """Test Task Manager Initialization.""" - # Act - task = EventTask() - app = TaskManager() - app_with_tasks = TaskManager(tasks=[task]) - # Assert - assert app.tasks == [] - assert app_with_tasks.tasks == [task] - - -async def test_task_manager_context() -> None: - """Test Task Manager Context.""" - # Arrange - event = Event() - task = EventTask(event=event) - app = TaskManager(tasks=[task]) - - # Act - event_before = event.is_set() - async with app: - event_in_context = event.is_set() - - # Assert - assert event_before is False - assert event_in_context is True - - -@pytest.mark.parametrize("auto_start", [True, False]) -async def test_task_manager_auto_start_disabled(*, auto_start: bool) -> None: - """Test Task Manager Auto Start Disabled.""" - # Arrange - event = Event() - task = EventTask(event=event) - app = TaskManager(auto_start=auto_start, tasks=[task]) - - # Act - event_before = event.is_set() - async with app: - event_in_context = event.is_set() - - # Assert - assert event_before is False - assert event_in_context is auto_start - - -async def test_task_manager_already_started_error() -> None: - """Test Task Manager Already Started Warning.""" - # Arrange - app = TaskManager() - - # Act / Assert - async with app: - with pytest.raises(TaskAddOperationError): - await app.start() - - -async def test_task_manager_out_of_context_errors() -> None: - """Test Task Manager Out of Context Errors.""" - # Arrange - app = TaskManager() - - # Act / Assert - with pytest.raises(OutOfContextError): - await app.start() - - with pytest.raises(OutOfContextError): - await app.__aexit__(None, None, None) diff --git a/.conflict-base-0/tests/task/test_router.py b/.conflict-base-0/tests/task/test_router.py deleted file mode 100644 index ed30af7..0000000 --- a/.conflict-base-0/tests/task/test_router.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Test Task Router.""" - -from functools import partial - -import pytest - -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.task import TaskRouter -from grelmicro.task._interval import IntervalTask -from grelmicro.task.errors import FunctionTypeError, TaskAddOperationError -from tests.task.samples import EventTask, SimpleClass, test1, test2, test3 - - -def test_router_init() -> None: - """Test Task Router Initialization.""" - # Arrange - custom_task = EventTask() - - # Act - router = TaskRouter() - router_with_task = TaskRouter(tasks=[custom_task]) - - # Assert - assert router.tasks == [] - assert router_with_task.tasks == [custom_task] - - -def test_router_add_task() -> None: - """Test Task Router Add Task.""" - # Arrange - custom_task1 = EventTask() - custom_task2 = EventTask() - router = TaskRouter() - router_with_task = TaskRouter(tasks=[custom_task1]) - - # Act - router.add_task(custom_task1) - router_with_task.add_task(custom_task2) - - # Assert - assert router.tasks == [custom_task1] - assert router_with_task.tasks == [custom_task1, custom_task2] - - -def test_router_include_router() -> None: - """Test Task Router Include Router.""" - # Arrange - custom_task1 = EventTask() - custom_task2 = EventTask() - router = TaskRouter(tasks=[custom_task1]) - router_with_task = TaskRouter(tasks=[custom_task2]) - - # Act - router.include_router(router_with_task) - - # Assert - assert router.tasks == [custom_task1, custom_task2] - - -def test_router_interval() -> None: - """Test Task Router add interval task.""" - # Arrange - task_count = 4 - custom_task = EventTask() - router = TaskRouter(tasks=[custom_task]) - sync = Lock(backend=MemorySyncBackend(), name="testlock") - - # Act - router.interval(name="test1", seconds=10, sync=sync)(test1) - router.interval(name="test2", seconds=20)(test2) - router.interval(seconds=10)(test3) - - # Assert - assert len(router.tasks) == task_count - assert ( - sum(isinstance(task, IntervalTask) for task in router.tasks) - == task_count - 1 - ) - assert router.tasks[0].name == "event_task" - assert router.tasks[1].name == "test1" - assert router.tasks[2].name == "test2" - assert router.tasks[3].name == "tests.task.samples:test3" - - -def test_router_interval_name_generation() -> None: - """Test Task Router Interval Name Generation.""" - # Arrange - router = TaskRouter() - - # Act - router.interval(seconds=10)(test1) - router.interval(seconds=10)(SimpleClass.static_method) - router.interval(seconds=10)(SimpleClass.method) - - # Assert - assert router.tasks[0].name == "tests.task.samples:test1" - assert ( - router.tasks[1].name == "tests.task.samples:SimpleClass.static_method" - ) - assert router.tasks[2].name == "tests.task.samples:SimpleClass.method" - - -def test_router_interval_name_generation_error() -> None: - """Test Task Router Interval Name Generation Error.""" - # Arrange - router = TaskRouter() - test_instance = SimpleClass() - - # Act - with pytest.raises(FunctionTypeError, match="nested function"): - - @router.interval(seconds=10) - def nested_function() -> None: - pass - - with pytest.raises(FunctionTypeError, match="lambda"): - router.interval(seconds=10)(lambda _: None) - - with pytest.raises(FunctionTypeError, match="method"): - router.interval(seconds=10)(test_instance.method) - - with pytest.raises(FunctionTypeError, match="partial()"): - router.interval(seconds=10)(partial(test1)) - - with pytest.raises( - FunctionTypeError, - match="callable without __module__ or __qualname__ attribute", - ): - router.interval(seconds=10)(object()) # type: ignore[arg-type] - - -def test_router_add_task_when_started() -> None: - """Test Task Router Add Task When Started.""" - # Arrange - custom_task = EventTask() - router = TaskRouter() - router.do_mark_as_started() - - # Act - with pytest.raises(TaskAddOperationError): - router.add_task(custom_task) - - -def test_router_include_router_when_started() -> None: - """Test Task Router Include Router When Started.""" - # Arrange - router = TaskRouter() - router.do_mark_as_started() - router_child = TaskRouter() - - # Act - with pytest.raises(TaskAddOperationError): - router.include_router(router_child) - - -def test_router_started_propagation() -> None: - """Test Task Router Started Propagation.""" - # Arrange - router = TaskRouter() - router_child = TaskRouter() - router.include_router(router_child) - - # Act - router_started_before = router.started() - router_child_started_before = router_child.started() - router.do_mark_as_started() - router_started_after = router.started() - router_child_started_after = router_child.started() - - # Assert - assert router_started_before is False - assert router_child_started_before is False - assert router_started_after is True - assert router_child_started_after is True diff --git a/.conflict-base-0/uv.lock b/.conflict-base-0/uv.lock deleted file mode 100644 index ff11a2b..0000000 --- a/.conflict-base-0/uv.lock +++ /dev/null @@ -1,1934 +0,0 @@ -version = 1 -requires-python = ">=3.11" -resolution-markers = [ - "python_full_version < '3.13'", - "python_full_version >= '3.13'", -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.6.2.post1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, -] - -[[package]] -name = "async-timeout" -version = "4.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, -] - -[[package]] -name = "asyncpg" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, - { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, - { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, - { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, - { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, - { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, - { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, - { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, - { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, - { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, - { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, - { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, - { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, - { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, - { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, - { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, - { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, - { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, - { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, - { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, - { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, - { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, - { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, - { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, -] - -[[package]] -name = "babel" -version = "2.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, -] - -[[package]] -name = "backports-tarfile" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, -] - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, - { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, - { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, - { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, - { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, - { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, - { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, - { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, - { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, - { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, - { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, - { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, - { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, - { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, - { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, - { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, - { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, - { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, - { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, - { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, - { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, - { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, - { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, - { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, - { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, - { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, - { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, - { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, - { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - -[[package]] -name = "click" -version = "8.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "coverage" -version = "7.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 }, - { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 }, - { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 }, - { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 }, - { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 }, - { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 }, - { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 }, - { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 }, - { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 }, - { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 }, - { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 }, - { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 }, - { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 }, - { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 }, - { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 }, - { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 }, - { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, - { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, - { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, - { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, - { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, - { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, - { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, - { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, - { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, - { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, - { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, - { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, - { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, - { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, - { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, - { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, - { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, - { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, - { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, - { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, - { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, - { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, - { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[package]] -name = "cryptography" -version = "43.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, - { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, - { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, - { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, - { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, - { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, - { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, - { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, - { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, - { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, - { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, - { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, -] - -[[package]] -name = "cyclic" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547 }, -] - -[[package]] -name = "distlib" -version = "0.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, -] - -[[package]] -name = "docker" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "requests" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, -] - -[[package]] -name = "fast-depends" -version = "2.4.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/f5/8b42b7588a67ad78991e5e7ca0e0c6a1ded535a69a725e4e48d3346a20c1/fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d", size = 16682 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/08/4adb160d8394053289fdf3b276e93b53271fd463e54fff8911b23c1db4ed/fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2", size = 17651 }, -] - -[[package]] -name = "fastapi" -version = "0.115.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/29/f71316b9273b6552a263748e49cd7b83898dc9499a663d30c7b9cb853cb8/fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", size = 301047 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/c4/148d5046a96c428464557264877ae5a9338a83bbe0df045088749ec89820/fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796", size = 94866 }, -] - -[[package]] -name = "fastapi-cli" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/1ad5ce32d029aeb9117e9a5a9b3e314a8477525d60c12a9b7730a3c186ec/fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", size = 15571 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/ea/4b5011012ac925fe2f83b19d0e09cee9d324141ec7bf5e78bb2817f96513/fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46", size = 9489 }, -] - -[[package]] -name = "faststream" -version = "0.5.30" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "fast-depends" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/d3/c2a3e1233274c93a4978cbac210a81ba05cee09e2e0051049b40f55406f1/faststream-0.5.30.tar.gz", hash = "sha256:50ad5288719cfa75c13e9c277d40afae62533a590facad6e6d215e868f2b97f4", size = 284478 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/ce/a9eec6c2c9803de6bc2b2a5cac35d56b8908c64fcdd4c73616c1a16c9b90/faststream-0.5.30-py3-none-any.whl", hash = "sha256:bf48826be99210f3e9c7dff1b2a17b4bc4762c873c5558ac81b9b873549ae6a1", size = 382011 }, -] - -[[package]] -name = "filelock" -version = "3.16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, -] - -[[package]] -name = "grelmicro" -version = "0.2.2" -source = { editable = "." } -dependencies = [ - { name = "anyio" }, - { name = "fast-depends" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, -] - -[package.optional-dependencies] -postgres = [ - { name = "asyncpg" }, -] -redis = [ - { name = "redis" }, -] -standard = [ - { name = "loguru" }, - { name = "orjson" }, -] - -[package.dev-dependencies] -dev = [ - { name = "fastapi" }, - { name = "fastapi-cli" }, - { name = "faststream" }, - { name = "hatch" }, - { name = "mdx-include" }, - { name = "mypy" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "pytest-randomly" }, - { name = "pytest-timeout" }, - { name = "ruff" }, - { name = "testcontainers", extra = ["redis"] }, -] -docs = [ - { name = "mkdocs-material" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, -] - -[package.metadata] -requires-dist = [ - { name = "anyio", specifier = ">=4.0.0" }, - { name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.30.0" }, - { name = "fast-depends", specifier = ">=2.0.0" }, - { name = "loguru", marker = "extra == 'standard'", specifier = ">=0.7.2" }, - { name = "orjson", marker = "extra == 'standard'", specifier = ">=3.10.11" }, - { name = "pydantic", specifier = ">=2.5.0" }, - { name = "pydantic-settings", specifier = ">=2.5.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "fastapi", specifier = ">=0.115.5" }, - { name = "fastapi-cli", specifier = ">=0.0.5" }, - { name = "faststream", specifier = ">=0.5.30" }, - { name = "hatch", specifier = ">=1.13.0" }, - { name = "mdx-include", specifier = ">=1.4.2" }, - { name = "mypy", specifier = ">=1.12.0" }, - { name = "pre-commit", specifier = ">=4.0.1" }, - { name = "pytest", specifier = ">=8.0.0" }, - { name = "pytest-cov", specifier = ">=6.0.0" }, - { name = "pytest-mock", specifier = ">=3.14.0" }, - { name = "pytest-randomly", specifier = ">=3.16.0" }, - { name = "pytest-timeout", specifier = ">=2.3.1" }, - { name = "ruff", specifier = ">=0.7.4" }, - { name = "testcontainers", extras = ["postgres", "redis"], specifier = ">=4.8.2" }, -] -docs = [ - { name = "mkdocs-material", specifier = ">=9.5.44" }, - { name = "pygments", specifier = ">=2.18.0" }, - { name = "pymdown-extensions", specifier = ">=10.12" }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, -] - -[[package]] -name = "hatch" -version = "1.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "hatchling" }, - { name = "httpx" }, - { name = "hyperlink" }, - { name = "keyring" }, - { name = "packaging" }, - { name = "pexpect" }, - { name = "platformdirs" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "tomli-w" }, - { name = "tomlkit" }, - { name = "userpath" }, - { name = "uv" }, - { name = "virtualenv" }, - { name = "zstandard" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/ed/5001de278f8d7381cbc84f5efdae72308fe37493bc063878f6a1ac07dab8/hatch-1.13.0.tar.gz", hash = "sha256:5e1a75770cfe8f3ebae3abfded3a976238b0acefd19cdabc5245597525b8066f", size = 5188060 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/8d/6d965a22bc38cec091ba82131624bb5d75471094d7fe05e829536de3de2f/hatch-1.13.0-py3-none-any.whl", hash = "sha256:bb1a18558a626279cae338b4d8a9d3ca4226d5e06d50de600608c57acd131b67", size = 125757 }, -] - -[[package]] -name = "hatchling" -version = "1.26.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "pathspec" }, - { name = "pluggy" }, - { name = "trove-classifiers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/47/7ec270a9567262ae3cb32dd420d2b53bf7aee769aca1f240eae0426b5bbc/hatchling-1.26.3.tar.gz", hash = "sha256:b672a9c36a601a06c4e88a1abb1330639ee8e721e0535a37536e546a667efc7a", size = 54968 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl", hash = "sha256:c407e1c6c17b574584a66ae60e8e9a01235ecb6dc61d01559bb936577aaf5846", size = 75773 }, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, -] - -[[package]] -name = "httptools" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, - { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, - { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, - { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, - { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, - { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, - { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, - { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, - { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, - { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, - { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, - { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, - { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, -] - -[[package]] -name = "httpx" -version = "0.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, -] - -[[package]] -name = "hyperlink" -version = "21.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, -] - -[[package]] -name = "identify" -version = "2.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/79/7a520fc5011e02ca3f3285b5f6820eaf80443eb73e3733f73c02fb42ba0b/identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd", size = 99113 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/86/c4395700f3c5475424fb5c41e20c16be28d10c904aee4d005ba3217fc8e7/identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", size = 98982 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, -] - -[[package]] -name = "jaraco-classes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, -] - -[[package]] -name = "jaraco-context" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, -] - -[[package]] -name = "jaraco-functools" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, -] - -[[package]] -name = "jeepney" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, -] - -[[package]] -name = "jinja2" -version = "3.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, -] - -[[package]] -name = "keyring" -version = "25.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, - { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "secretstorage", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/24/64447b13df6a0e2797b586dad715766d756c932ce8ace7f67bd384d76ae0/keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6", size = 62675 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/c9/353c156fa2f057e669106e5d6bcdecf85ef8d3536ce68ca96f18dc7b6d6f/keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741", size = 39096 }, -] - -[[package]] -name = "loguru" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549 }, -] - -[[package]] -name = "markdown" -version = "3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "mdx-include" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cyclic" }, - { name = "markdown" }, - { name = "rcslice" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591 }, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, -] - -[[package]] -name = "mkdocs-material" -version = "9.5.44" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/56/182d8121db9ab553cdf9bc58d5972b89833f60b63272f693c1f2b849b640/mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0", size = 3964306 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/eb/a801d00e0e210d82184aacce596906ec065422c78a7319244ba0771c4ded/mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca", size = 8674509 }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, -] - -[[package]] -name = "more-itertools" -version = "10.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, -] - -[[package]] -name = "mypy" -version = "1.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/03/744330105a74dc004578f47ec27e1bf66b1dd5664ea444d18423e41343bd/mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d", size = 3150767 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/0a/70de7c97a86cb85535077ab5cef1cbc4e2812fd2e9cc21d78eb561a6b80f/mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", size = 10940998 }, - { url = "https://files.pythonhosted.org/packages/c0/97/9ed6d4834d7549936ab88533b302184fb568a0940c4000d2aaee6dc07112/mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", size = 10108523 }, - { url = "https://files.pythonhosted.org/packages/48/41/1686f37d09c915dfc5b683e20cc99dabac199900b5ca6d22747b99ddcb50/mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", size = 12505553 }, - { url = "https://files.pythonhosted.org/packages/8d/2b/2dbcaa7e97b23f27ced77493256ee878f4a140ac750e198630ff1b9b60c6/mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", size = 12988634 }, - { url = "https://files.pythonhosted.org/packages/54/55/710d082e91a2ccaea21214229b11f9215a9d22446f949491b5457655e82b/mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", size = 9630747 }, - { url = "https://files.pythonhosted.org/packages/8a/74/b9e0e4f06e951e277058f878302faa154d282ca11274c59fe08353f52949/mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", size = 11079902 }, - { url = "https://files.pythonhosted.org/packages/9f/62/fcad290769db3eb0de265094cef5c94d6075c70bc1e42b67eee4ca192dcc/mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", size = 10072373 }, - { url = "https://files.pythonhosted.org/packages/cb/27/9ac78349c2952e4446288ec1174675ab9e0160ed18c2cb1154fa456c54e8/mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", size = 12589779 }, - { url = "https://files.pythonhosted.org/packages/7c/4a/58cebd122cf1cba95680ac51303fbeb508392413ca64e3e711aa7d4877aa/mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", size = 13044459 }, - { url = "https://files.pythonhosted.org/packages/5b/c7/672935e2a3f9bcc07b1b870395a653f665657bef3cdaa504ad99f56eadf0/mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", size = 9731919 }, - { url = "https://files.pythonhosted.org/packages/bb/b0/092be5094840a401940c95224f63bb2a8f09bce9251ac1df180ec523830c/mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", size = 11068611 }, - { url = "https://files.pythonhosted.org/packages/9a/86/f20f53b8f062876c39602243d7a59b5cabd6b24315d8de511d607fa4de6a/mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", size = 10068036 }, - { url = "https://files.pythonhosted.org/packages/84/c7/1dbd6575785522da1d4c1ac2c419505fcf23bee74811880cac447a4a77ab/mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", size = 12585671 }, - { url = "https://files.pythonhosted.org/packages/46/8a/f6ae18b446eb2bccce54c4bd94065bcfe417d6c67021dcc032bf1e720aff/mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", size = 13036083 }, - { url = "https://files.pythonhosted.org/packages/59/e6/fc65fde3dc7156fce8d49ba21c7b1f5d866ad50467bf196ca94a7f6d2c9e/mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", size = 9735467 }, - { url = "https://files.pythonhosted.org/packages/84/6b/1db9de4e0764778251fb2d64cb7455cf6db75dc99c9f72c8b7e74b6a8a17/mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", size = 2646060 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, -] - -[[package]] -name = "orjson" -version = "3.10.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/3a/10320029954badc7eaa338a15ee279043436f396e965dafc169610e4933f/orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", size = 5444879 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/25/c869a1fbd481dcb02c70032fd6a7243de7582bc48c7cae03d6f0985a11c0/orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6", size = 266432 }, - { url = "https://files.pythonhosted.org/packages/6a/a4/2307155ee92457d28345308f7d8c0e712348404723025613adeffcb531d0/orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe", size = 151884 }, - { url = "https://files.pythonhosted.org/packages/aa/82/daf1b2596dd49fe44a1bd92367568faf6966dcb5d7f99fd437c3d0dc2de6/orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67", size = 167371 }, - { url = "https://files.pythonhosted.org/packages/63/a8/680578e4589be5fdcfe0186bdd7dc6fe4a39d30e293a9da833cbedd5a56e/orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b", size = 154368 }, - { url = "https://files.pythonhosted.org/packages/6e/ce/9cb394b5b01ef34579eeca6d704b21f97248f607067ce95a24ba9ea2698e/orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d", size = 165725 }, - { url = "https://files.pythonhosted.org/packages/49/24/55eeb05cfb36b9e950d05743e6f6fdb7d5f33ca951a27b06ea6d03371aed/orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5", size = 142522 }, - { url = "https://files.pythonhosted.org/packages/94/0c/3a6a289e56dcc9fe67dc6b6d33c91dc5491f9ec4a03745efd739d2acf0ff/orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a", size = 146934 }, - { url = "https://files.pythonhosted.org/packages/1d/5c/a08c0e90a91e2526029a4681ff8c6fc4495b8bab77d48801144e378c7da9/orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981", size = 142904 }, - { url = "https://files.pythonhosted.org/packages/2c/c9/710286a60b14e88288ca014d43befb08bb0a4a6a0f51b875f8c2f05e8205/orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55", size = 144459 }, - { url = "https://files.pythonhosted.org/packages/7d/68/ef7b920e0a09e02b1a30daca1b4864938463797995c2fabe457c1500220a/orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec", size = 136444 }, - { url = "https://files.pythonhosted.org/packages/78/f2/a712dbcef6d84ff53e13056e7dc69d9d4844bd1e35e51b7431679ddd154d/orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51", size = 266505 }, - { url = "https://files.pythonhosted.org/packages/94/54/53970831786d71f98fdc13c0f80451324c9b5c20fbf42f42ef6147607ee7/orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97", size = 151745 }, - { url = "https://files.pythonhosted.org/packages/35/38/482667da1ca7ef95d44d4d2328257a144fd2752383e688637c53ed474d2a/orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19", size = 167274 }, - { url = "https://files.pythonhosted.org/packages/23/2f/5bb0a03e819781d82dadb733fde8ebbe20d1777d1a33715d45ada4d82ce8/orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0", size = 154605 }, - { url = "https://files.pythonhosted.org/packages/49/e9/14cc34d45c7bd51665aff9b1bb6b83475a61c52edb0d753fffe1adc97764/orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433", size = 165874 }, - { url = "https://files.pythonhosted.org/packages/7b/61/c2781ecf90f99623e97c67a31e8553f38a1ecebaf3189485726ac8641576/orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5", size = 142813 }, - { url = "https://files.pythonhosted.org/packages/4d/4f/18c83f78b501b6608569b1610fcb5a25c9bb9ab6a7eb4b3a55131e0fba37/orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd", size = 146762 }, - { url = "https://files.pythonhosted.org/packages/ba/19/ea80d5b575abd3f76a790409c2b7b8a60f3fc9447965c27d09613b8bddf4/orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b", size = 143186 }, - { url = "https://files.pythonhosted.org/packages/2c/f5/d835fee01a0284d4b78effc24d16e7609daac2ff6b6851ca1bdd3b6194fc/orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d", size = 144489 }, - { url = "https://files.pythonhosted.org/packages/03/60/748e0e205060dec74328dfd835e47902eb5522ae011766da76bfff64e2f4/orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284", size = 136614 }, - { url = "https://files.pythonhosted.org/packages/13/92/400970baf46b987c058469e9e779fb7a40d54a5754914d3634cca417e054/orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", size = 266402 }, - { url = "https://files.pythonhosted.org/packages/3c/fa/f126fc2d817552bd1f67466205abdcbff64eab16f6844fe6df2853528675/orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", size = 140826 }, - { url = "https://files.pythonhosted.org/packages/ad/18/9b9664d7d4af5b4fe9fe6600b7654afc0684bba528260afdde10c4a530aa/orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", size = 142593 }, - { url = "https://files.pythonhosted.org/packages/20/f9/a30c68f12778d5e58e6b5cdd26f86ee2d0babce1a475073043f46fdd8402/orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", size = 146777 }, - { url = "https://files.pythonhosted.org/packages/f2/97/12047b0c0e9b391d589fb76eb40538f522edc664f650f8e352fdaaf77ff5/orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", size = 142961 }, - { url = "https://files.pythonhosted.org/packages/a4/97/d904e26c1cabf2dd6ab1b0909e9b790af28a7f0fcb9d8378d7320d4869eb/orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", size = 144486 }, - { url = "https://files.pythonhosted.org/packages/42/62/3760bd1e6e949321d99bab238d08db2b1266564d2f708af668f57109bb36/orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", size = 136361 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, -] - -[[package]] -name = "pre-commit" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - -[[package]] -name = "pydantic" -version = "2.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, -] - -[[package]] -name = "pydantic-core" -version = "2.23.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, - { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, - { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, - { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, - { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, - { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, - { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, - { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, - { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, - { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, - { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, - { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, - { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, - { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, - { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, - { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, - { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, - { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, - { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, - { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, - { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, - { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, - { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, - { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, - { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, - { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, - { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, - { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, - { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, - { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, - { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, - { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, - { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, - { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, - { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, - { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, -] - -[[package]] -name = "pygments" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/32f05854cfd432e9286bb41a870e0d1a926b72df5f5cdb6dec962b2e369e/pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7", size = 840790 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/32/95a164ddf533bd676cbbe878e36e89b4ade3efde8dd61d0148c90cbbe57e/pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", size = 263448 }, -] - -[[package]] -name = "pytest" -version = "8.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, -] - -[[package]] -name = "pytest-cov" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, -] - -[[package]] -name = "pytest-mock" -version = "3.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, -] - -[[package]] -name = "pytest-randomly" -version = "3.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396 }, -] - -[[package]] -name = "pytest-timeout" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - -[[package]] -name = "pywin32" -version = "308" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156 }, - { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559 }, - { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495 }, - { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, - { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, - { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, - { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, - { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, - { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, -] - -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, -] - -[[package]] -name = "rcslice" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180 }, -] - -[[package]] -name = "redis" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, -] - -[[package]] -name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "rich" -version = "13.9.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", size = 222889 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", size = 242157 }, -] - -[[package]] -name = "ruff" -version = "0.7.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, - { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, - { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, - { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, - { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, - { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, - { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, - { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, - { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, - { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, - { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, - { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, - { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, - { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, - { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, - { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, - { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, -] - -[[package]] -name = "secretstorage" -version = "3.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - -[[package]] -name = "six" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "starlette" -version = "0.41.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, -] - -[[package]] -name = "testcontainers" -version = "4.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docker" }, - { name = "typing-extensions" }, - { name = "urllib3" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/72/c58d84f5704c6caadd9f803a3adad5ab54ac65328c02d13295f40860cf33/testcontainers-4.8.2.tar.gz", hash = "sha256:dd4a6a2ea09e3c3ecd39e180b6548105929d0bb78d665ce9919cb3f8c98f9853", size = 63590 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/77/5ac0dff2903a033d83d971fd85957356abdb66a327f3589df2b3d1a586b4/testcontainers-4.8.2-py3-none-any.whl", hash = "sha256:9e19af077cd96e1957c13ee466f1f32905bc6c5bc1bc98643eb18be1a989bfb0", size = 104326 }, -] - -[package.optional-dependencies] -redis = [ - { name = "redis" }, -] - -[[package]] -name = "tomli" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, -] - -[[package]] -name = "tomli-w" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, -] - -[[package]] -name = "tomlkit" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, -] - -[[package]] -name = "trove-classifiers" -version = "2024.10.21.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/85/92c2667cf221b37648041ce9319427f92fa76cbec634aad844e67e284706/trove_classifiers-2024.10.21.16.tar.gz", hash = "sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3", size = 16153 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl", hash = "sha256:0fb11f1e995a757807a8ef1c03829fbd4998d817319abcef1f33165750f103be", size = 13546 }, -] - -[[package]] -name = "typer" -version = "0.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] - -[[package]] -name = "userpath" -version = "1.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065 }, -] - -[[package]] -name = "uv" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/ad/66cc8e00c217e7fcf76598c880632b480aa38d4cad311596b78e99737498/uv-0.5.4.tar.gz", hash = "sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00", size = 2315678 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/3e/6bf24d7bb0d11715ea783ecabcacdecdc8c51fca0144fcdad2090d65bae5/uv-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2", size = 13853445 }, - { url = "https://files.pythonhosted.org/packages/b8/be/c3acbe2944cd694a5d61a7a461468fa886512c84014545bb8f3244092eaa/uv-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58", size = 13969300 }, - { url = "https://files.pythonhosted.org/packages/1f/c5/06e3b93045179b92d75cf94e6e224baec3226070f1cbc0e11d4898300b54/uv-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4", size = 12932325 }, - { url = "https://files.pythonhosted.org/packages/b8/f9/06ab86e9f0c270c495077ef2b588458172ed84f9c337de725c8b08872354/uv-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39", size = 13183356 }, - { url = "https://files.pythonhosted.org/packages/c1/cb/bee01ef23e5020dc1f12d86ca8f82e95a723585db3ec64bfab4016e5616c/uv-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6", size = 13622310 }, - { url = "https://files.pythonhosted.org/packages/19/4b/128fd874151919c71af51f528db28964e6d8e509fff12210ec9ba99b13fb/uv-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263", size = 14207832 }, - { url = "https://files.pythonhosted.org/packages/b1/2b/0fed8a49440494f6806dcb67021ca8f14d46f45a665235fc153791e19574/uv-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83", size = 14878796 }, - { url = "https://files.pythonhosted.org/packages/c9/35/a6dc404d4d8884e26ad7bda004c101972fe7d81f86546a8628272812b897/uv-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25", size = 14687838 }, - { url = "https://files.pythonhosted.org/packages/74/9e/c2ebf66b90d48def06cda29626bb38068418ed135ca903beb293825ef66d/uv-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927", size = 18960541 }, - { url = "https://files.pythonhosted.org/packages/3d/67/28a8b4c23920ae1b1b0103ebae2fa176bd5677c4353b5e814a51bd183285/uv-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194", size = 14471756 }, - { url = "https://files.pythonhosted.org/packages/e9/1c/9698818f4c5493dfd5ab0899a90eee789cac214de2f171220bcdfaefc93a/uv-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01", size = 13389089 }, - { url = "https://files.pythonhosted.org/packages/0b/30/31a9985d84ffb63fb9212fa2b565497e0ceb581be055e5cc760afbe26b11/uv-0.5.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c", size = 13612748 }, - { url = "https://files.pythonhosted.org/packages/26/8d/bae613187ba88d74f0268246ce140f23d399bab96d2cbc055d6e4adafd09/uv-0.5.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876", size = 13946421 }, - { url = "https://files.pythonhosted.org/packages/0e/22/efd1eec81a566139bced68f4bd140c275edac3dac1bd6236cf8d756423db/uv-0.5.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119", size = 15752913 }, - { url = "https://files.pythonhosted.org/packages/49/b2/0cc4ae143b9605c25e75772aea22876b5875db79982ba62bb6f8d3099fab/uv-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc", size = 14599503 }, - { url = "https://files.pythonhosted.org/packages/51/9a/33d40a5068fd37c4f7b4fa82396e3ee90a691cd256f364ff398612c1d5d4/uv-0.5.4-py3-none-win32.whl", hash = "sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa", size = 13749570 }, - { url = "https://files.pythonhosted.org/packages/b1/c8/827e4da65cbdab2c1619767a68ab99a31de078e511b71ca9f24777df33f9/uv-0.5.4-py3-none-win_amd64.whl", hash = "sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb", size = 15573613 }, -] - -[[package]] -name = "uvicorn" -version = "0.32.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, -] - -[[package]] -name = "virtualenv" -version = "20.27.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838 }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, -] - -[[package]] -name = "watchfiles" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, - { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, - { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, - { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, - { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, - { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, - { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, - { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, - { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, - { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, - { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, - { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, - { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, - { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, - { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, - { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, - { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, - { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, - { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, - { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, - { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, - { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, - { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, - { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, - { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, - { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, - { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, - { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, - { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, - { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, - { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, - { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, - { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, - { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, - { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, - { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, - { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, - { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, -] - -[[package]] -name = "websockets" -version = "14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, - { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, - { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, - { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, - { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, - { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, - { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, - { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, - { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, - { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, - { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, - { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, - { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, - { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, - { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, - { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, - { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, - { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, - { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, - { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, - { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, - { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, - { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, - { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, - { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, - { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, - { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, - { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, - { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, - { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, - { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, - { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, - { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, - { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, -] - -[[package]] -name = "win32-setctime" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", size = 3676 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad", size = 3604 }, -] - -[[package]] -name = "wrapt" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313 }, - { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164 }, - { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890 }, - { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118 }, - { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746 }, - { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668 }, - { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556 }, - { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712 }, - { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327 }, - { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523 }, - { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614 }, - { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316 }, - { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322 }, - { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055 }, - { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291 }, - { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374 }, - { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896 }, - { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738 }, - { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568 }, - { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653 }, - { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, -] - -[[package]] -name = "zipp" -version = "3.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, -] - -[[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 }, -] diff --git a/.conflict-files b/.conflict-files deleted file mode 100644 index 22d1b75..0000000 --- a/.conflict-files +++ /dev/null @@ -1,3 +0,0 @@ -ancestorEntries = ["grelmicro/sync/postgres.py"] -ourEntries = ["grelmicro/sync/postgres.py"] -theirEntries = ["grelmicro/sync/postgres.py"] diff --git a/.conflict-side-0/.github/workflows/ci.yml b/.conflict-side-0/.github/workflows/ci.yml deleted file mode 100644 index 5fb99bb..0000000 --- a/.conflict-side-0/.github/workflows/ci.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: CI - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras - - - name: Run Mypy - run: uv run mypy . - - test: - name: Test Python ${{ matrix.python }} - runs-on: "ubuntu-latest" - strategy: - fail-fast: true - matrix: - python: ["3.11", "3.12", "3.13"] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras --python ${{ matrix.python }} - - - name: Run unit tests - run: uv run pytest -x - - - name: Run integration tests - run: uv run pytest -x -m integration --cov-append - - - name: Rename coverage report - run: mv .coverage .coverage.py${{ matrix.python }} - - - name: Save coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-${{ matrix.python }} - path: .coverage.py${{ matrix.python }} - include-hidden-files: true - - coverage-report: - name: Coverage report - runs-on: ubuntu-latest - needs: [test] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Get coverage reports - uses: actions/download-artifact@v4 - with: - pattern: coverage-* - merge-multiple: true - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras - - - name: Combine coverage reports - run: | - uv run coverage combine .coverage.* - uv run coverage xml -o cov.xml - - - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./cov.xml diff --git a/.conflict-side-0/.github/workflows/release.yml b/.conflict-side-0/.github/workflows/release.yml deleted file mode 100644 index c8d4bab..0000000 --- a/.conflict-side-0/.github/workflows/release.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Release - -on: - release: - types: - - published - -jobs: - bump-version: - name: Bump version - runs-on: ubuntu-latest - steps: - - - name: Generate GitHub App Token - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ secrets.GRELINFO_ID }} - private-key: ${{ secrets.GRELINFO_KEY }} - - - name: Get GitHub App User ID - id: user-id - run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - - - name: Configure Git App Credentials - run: | - git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' - git config --global user.email '${{ steps.user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}@users.noreply.github.com>' - - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref_name }} - token: ${{ steps.app-token.outputs.token }} - persist-credentials: false - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Get release version - id: release-version - run: echo "release-version=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" - - - name: Get current version - id: current-version - run: echo "current-version=$(uv run hatch version)" >> "$GITHUB_OUTPUT" - - - name: Bump version if necessary - if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} - run: | - uv run hatch version $RELEASE_VERSION - uv lock - - - name: Commit and push changes - if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} - run: | - git add . - git commit -m "🚀 Release $RELEASE_VERSION" - git tag -f $RELEASE_VERSION - git push origin $RELEASE_VERSION --force - git push origin HEAD:main - - publish-docs: - runs-on: ubuntu-latest - needs: [bump-version] - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.ref_name }} - - - name: Configure Git Credentials - run: | - git config user.name "${GITHUB_ACTOR}" - git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --group docs - - - name: Deploy docs on GitHub Pages - run: uv run mkdocs gh-deploy --force - - publish-pypi: - needs: [bump-version] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.ref_name }} - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Build - run: uv build - - - name: Publish - run: uv publish -t ${{ secrets.PYPI_TOKEN }} diff --git a/.conflict-side-0/.gitignore b/.conflict-side-0/.gitignore deleted file mode 100644 index 0d118ab..0000000 --- a/.conflict-side-0/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Python-generated files -__pycache__/ -*.py[oc] -build/ -dist/ -wheels/ -*.egg-info - -# Virtual environments -.venv - -# Coverage -cov.xml -.coverage - -# Mkdocs -site/ diff --git a/.conflict-side-0/.pre-commit-config.yaml b/.conflict-side-0/.pre-commit-config.yaml deleted file mode 100644 index 5e5a141..0000000 --- a/.conflict-side-0/.pre-commit-config.yaml +++ /dev/null @@ -1,63 +0,0 @@ -default_language_version: - python: python3.11 - -repos: - -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: end-of-file-fixer - - id: check-toml - - id: check-yaml - - id: check-added-large-files - - id: trailing-whitespace - -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 - hooks: - - id: ruff - args: [ --fix ] - - id: ruff-format - -- repo: https://github.com/codespell-project/codespell - rev: v2.3.0 - hooks: - - id: codespell - -- repo: local - hooks: - - - id: readme-to-docs - name: readme-to-docs - description: "Copy README.md to docs/index.md" - entry: cp README.md docs/index.md - language: system - pass_filenames: false - - # --- Local development hooks --- - - id: uv-lock - name: uv-lock - description: "Lock dependencies with 'uv lock'" - entry: uv lock - language: system - pass_filenames: false - - - id: mypy - name: mypy - description: "Run 'mypy' for static type checking" - entry: uv run mypy - language: system - types: [python] - require_serial: true - - - id: pytest - name: pytest - description: "Run 'pytest' for unit testing" - entry: uv run pytest --cov-fail-under=90 - language: system - pass_filenames: false - -ci: - autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate - skip: [uv-lock, mypy, pytest] diff --git a/.conflict-side-0/.vscode/settings.json b/.conflict-side-0/.vscode/settings.json deleted file mode 100644 index 806ffc4..0000000 --- a/.conflict-side-0/.vscode/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - // Editor settings - "editor.rulers": [80, 100], - "files.trimTrailingWhitespace": true, - "terminal.integrated.scrollback": 10000, - - // Files exclude settings - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "**/__pycache__": true, - "**/.venv": true, - "**/.mypy_cache": true, - "**/.pytest_cache": true, - "**/.ruff_cache": true, - ".coverage": true - }, - - // Python settings - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.terminal.activateEnvInCurrentTerminal": true, - "python.terminal.activateEnvironment": true, - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "python.testing.pytestArgs": ["--no-cov", "--color=yes"], - "python.analysis.inlayHints.pytestParameters": true, - - // Python editor settings - "[python]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff" - }, - - // Mypy settings - "mypy-type-checker.importStrategy": "fromEnvironment", - - // YAML settings - "yaml.schemas": { - "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" - }, - "yaml.customTags": [ - "!ENV scalar", - "!ENV sequence", - "!relative scalar", - "tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg", - "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", - "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", - "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" - ], - - // Ruff settings - "ruff.configurationPreference": "filesystemFirst" -} diff --git a/.conflict-side-0/LICENSE b/.conflict-side-0/LICENSE deleted file mode 100644 index 18dafa2..0000000 --- a/.conflict-side-0/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Loïc Gremaud - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/.conflict-side-0/README.md b/.conflict-side-0/README.md deleted file mode 100644 index 9f3e0ff..0000000 --- a/.conflict-side-0/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# Grelmicro - -Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. - -It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. - -[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) -[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) -[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) - -______________________________________________________________________ - -**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) - -**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) - -______________________________________________________________________ - -## Overview - -Grelmicro provides essential features for building robust distributed systems, including: - -- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. -- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. -- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. -- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. - -These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. - -### Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. - -### Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. - -The available primitives are: - -- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **Lock**: A distributed lock that can be used to synchronize access to shared resources. - -### Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Installation - -```bash -pip install grelmicro -``` - -## Examples - -### FastAPI Integration - -- Create a file `main.py` with: - -```python -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") -``` - -## Dependencies - -Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. - -### `standard` Dependencies - -When you install Grelmicro with `pip install grelmicro[standard]` it comes with: - -- `loguru`: A Python logging library. -- `orjson`: A fast, correct JSON library for Python. - -### `redis` Dependencies - -When you install Grelmicro with `pip install grelmicro[redis]` it comes with: - -- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). - -### `postgres` Dependencies - -When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: - -- `asyncpg`: The Python `asyncio` interface for PostgreSQL. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/.conflict-side-0/docs/index.md b/.conflict-side-0/docs/index.md deleted file mode 100644 index 9f3e0ff..0000000 --- a/.conflict-side-0/docs/index.md +++ /dev/null @@ -1,158 +0,0 @@ -# Grelmicro - -Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. - -It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. - -[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) -[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) -[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) - -______________________________________________________________________ - -**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) - -**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) - -______________________________________________________________________ - -## Overview - -Grelmicro provides essential features for building robust distributed systems, including: - -- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. -- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. -- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. -- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. - -These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. - -### Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. - -### Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. - -The available primitives are: - -- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **Lock**: A distributed lock that can be used to synchronize access to shared resources. - -### Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Installation - -```bash -pip install grelmicro -``` - -## Examples - -### FastAPI Integration - -- Create a file `main.py` with: - -```python -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") -``` - -## Dependencies - -Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. - -### `standard` Dependencies - -When you install Grelmicro with `pip install grelmicro[standard]` it comes with: - -- `loguru`: A Python logging library. -- `orjson`: A fast, correct JSON library for Python. - -### `redis` Dependencies - -When you install Grelmicro with `pip install grelmicro[redis]` it comes with: - -- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). - -### `postgres` Dependencies - -When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: - -- `asyncpg`: The Python `asyncio` interface for PostgreSQL. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/.conflict-side-0/docs/logging.md b/.conflict-side-0/docs/logging.md deleted file mode 100644 index 4575b03..0000000 --- a/.conflict-side-0/docs/logging.md +++ /dev/null @@ -1,73 +0,0 @@ -# Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to stdout. It supports JSON formatting and allows log level configuration via environment variables. - -## Dependencies - -For the moment the `logging` package is only working with the `loguru` Python logging library. -When `orjson` is installed, it will be used as the default JSON serializer for faster performance, otherwise, the standard `json` library will be used. - -[**Loguru**](https://loguru.readthedocs.io/en/stable/overview.html) is used as the logging library. - -For using `logging` package, please install the required dependencies: - -=== "Standard" - ```bash - pip install grelmicro[standard] - ``` - -=== "only loguru (minimum)" - ```bash - pip install loguru - ``` - -=== "loguru and orjson (manual)" - ```bash - pip install loguru orjson - ``` - - -## Configure Logging - -Just call the `configure_logging` function to set up the logging system. - -```python -{!> ../examples/logging/configure_logging.py!} -``` - -### Settings - -You can change the default settings using the following environment variables: - -- `LOG_LEVEL`: Set the desired log level (default: `INFO`). -- `LOG_FORMAT`: Choose the log format. Options are `TEXT` and `JSON`, or you can provide a custom [loguru](https://loguru.readthedocs.io/en/stable/overview.html) template (default: `TEXT`). - - -## Examples - -### Basic Usage - -Here is a quick example of how to use the logging system: - -```python -{!> ../examples/logging/basic.py!} -``` - -The console output, `stdout` will be: - -```json -{!> ../examples/logging/basic.log!} -``` - -### FastAPI Integration - -You can use the logging system with FastAPI as well: - -```python -{!> ../examples/logging/fastapi.py!} -``` - -!!! warning - It is crucial to call `configure_logging` during the lifespan of the FastAPI application. Failing to do so may result in the FastAPI CLI resetting the logging configuration. diff --git a/.conflict-side-0/docs/sync.md b/.conflict-side-0/docs/sync.md deleted file mode 100644 index 4c3b881..0000000 --- a/.conflict-side-0/docs/sync.md +++ /dev/null @@ -1,81 +0,0 @@ -# Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends (see more in the Backends section). - -The available primitives are: - -- **[Leader Election](#leader-election)**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **[Lock](#lock)**: A distributed lock that can be used to synchronize access to shared resources. - -The synchronization primitives can be used in combination with the `TaskManager` and `TaskRouter` to control task execution in a distributed system (see more in [Task Scheduler](task.md)). - -## Backend - -You must load a synchronization backend before using synchronization primitives. - -!!! note - Although Grelmicro use AnyIO for concurrency, the backends generally depend on `asyncio`, therefore Trio is not supported. - -You can initialize a backend like this: - -=== "Redis" - ```python - {!> ../examples/sync/redis.py!} - ``` - -=== "Postgres" - ```python - {!> ../examples/sync/postgres.py!} - ``` - -=== "Memory (For Testing Only)" - ```python - {!> ../examples/sync/memory.py!} - ``` - -!!! warning - Please make sure to use a proper way to store connection url, such as environment variables (not like the example above). - -!!! tip - Feel free to create your own backend and contribute it. In the `sync.abc` module, you can find the protocol for creating new backends. - - - -## Leader Election - -Leader election ensures that only one worker in the cluster is designated as the leader at any given time using a distributed lock. - -The leader election service is responsible for acquiring and renewing the distributed lock. It runs as an AnyIO Task that can be easily started with the [Task Manager](./task.md#task-manager). This service operates in the background, automatically renewing the lock to prevent other workers from acquiring it. The lock is released automatically when the task is cancelled or during shutdown. - -=== "Task Manager (Recommended)" - ```python - {!> ../examples/sync/leaderelection_task.py!} - ``` - -=== "AnyIO Task Group (Advanced)" - ```python - {!> ../examples/sync/leaderelection_anyio.py!} - ``` - -## Lock - -The lock is a distributed lock that can be used to synchronize access to shared resources. - -The lock supports the following features: - -- **Async**: The lock must be acquired and released asynchronously. -- **Distributed**: The lock must be distributed across multiple workers. -- **Reentrant**: The lock must allow the same token to acquire it multiple times to extend the lease. -- **Expiring**: The lock must have a timeout to auto-release after an interval to prevent deadlocks. -- **Non-blocking**: Lock operations must not block the async event loop. -- **Vendor-agnostic**: Must support multiple backends (Redis, Postgres, ConfigMap, etc.). - - -```python -{!> ../examples/sync/lock.py!} -``` - -!!! warning - The lock is designed for use within an async event loop and is not thread-safe or process-safe. diff --git a/.conflict-side-0/docs/task.md b/.conflict-side-0/docs/task.md deleted file mode 100644 index b6f0e00..0000000 --- a/.conflict-side-0/docs/task.md +++ /dev/null @@ -1,85 +0,0 @@ -# Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Task Manager - -The `TaskManager` class is the main entry point to manage scheduled tasks. You need to start the task manager to run the scheduled tasks using the application lifespan. - -=== "FastAPI" - - ```python - {!> ../examples/task/fastapi.py!} - ``` - -=== "FastStream" - - ```python - - {!> ../examples/task/faststream.py!} - ``` - -## Interval Task - -To create an `IntervalTask`, use the `interval` decorator method of the `TaskManager` instance. This decorator allows tasks to run at specified intervals. - -> **Note**: The interval specifies the waiting time between task executions. Ensure that the task execution duration is considered to meet deadlines effectively. - -=== "TaskManager" - - ```python - {!> ../examples/task/interval_manager.py!} - ``` - -=== "TaskRouter" - - ```python - {!> ../examples/task/interval_router.py!} - ``` - - -## Synchronization - -The Task can be synchronized using a [Synchoronization Primitive](sync.md) to control concurrency and manage simultaneous task execution. - -=== "Lock" - - ```python - {!> ../examples/task/lock.py!} - ``` - - -=== "Leader Election" - - - ```python - {!> ../examples/task/leaderelection.py!} - ``` - -## Task Router - -For bigger applications, you can use the `TaskRouter` class to manage tasks in different modules. - - -```python -{!> ../examples/task/router.py [ln:1-10]!} -``` - -Then you can include the `TaskRouter` into the `TaskManager` or other routers using the `include_router` method. - -```python -{!> ../examples/task/router.py [ln:12-]!} -``` - -!!! tip - The `TaskRouter` follows the same philosophy as the `APIRouter` in FastAPI or the **Router** in FastStream. diff --git a/.conflict-side-0/examples/__init__.py b/.conflict-side-0/examples/__init__.py deleted file mode 100644 index 73b7d32..0000000 --- a/.conflict-side-0/examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Examples.""" diff --git a/.conflict-side-0/examples/logging/__init__.py b/.conflict-side-0/examples/logging/__init__.py deleted file mode 100644 index bf04afe..0000000 --- a/.conflict-side-0/examples/logging/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Logging Examples.""" diff --git a/.conflict-side-0/examples/logging/basic.log b/.conflict-side-0/examples/logging/basic.log deleted file mode 100644 index 33c8e37..0000000 --- a/.conflict-side-0/examples/logging/basic.log +++ /dev/null @@ -1,4 +0,0 @@ -{"time":"2024-11-25T15:56:36.066922+01:00","level":"INFO","thread":"MainThread","logger":"__main__::7","msg":"This is an info message"} -{"time":"2024-11-25T15:56:36.067063+01:00","level":"WARNING","thread":"MainThread","logger":"__main__::8","msg":"This is a warning message with context","ctx":{"user":"Alice"}} -{"time":"2024-11-25T15:56:36.067105+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::9","msg":"This is an error message with context","ctx":{"user":"Bob"}} -{"time":"2024-11-25T15:56:36.067134+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::14","msg":"This is an exception message with context","ctx":{"user":"Charlie","exception":"ValueError: This is an exception"}} diff --git a/.conflict-side-0/examples/logging/basic.py b/.conflict-side-0/examples/logging/basic.py deleted file mode 100644 index 889f160..0000000 --- a/.conflict-side-0/examples/logging/basic.py +++ /dev/null @@ -1,17 +0,0 @@ -from loguru import logger - -from grelmicro.logging import configure_logging - -configure_logging() - -logger.debug("This is a debug message") -logger.info("This is an info message") -logger.warning("This is a warning message with context", user="Alice") -logger.error("This is an error message with context", user="Bob") - -try: - raise ValueError("This is an exception message") -except ValueError: - logger.exception( - "This is an exception message with context", user="Charlie" - ) diff --git a/.conflict-side-0/examples/logging/configure_logging.py b/.conflict-side-0/examples/logging/configure_logging.py deleted file mode 100644 index 0ffacd8..0000000 --- a/.conflict-side-0/examples/logging/configure_logging.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.logging import configure_logging - -configure_logging() diff --git a/.conflict-side-0/examples/logging/fastapi.py b/.conflict-side-0/examples/logging/fastapi.py deleted file mode 100644 index 7f318c5..0000000 --- a/.conflict-side-0/examples/logging/fastapi.py +++ /dev/null @@ -1,22 +0,0 @@ -from contextlib import asynccontextmanager - -from fastapi import FastAPI -from loguru import logger - -from grelmicro.logging import configure_logging - - -@asynccontextmanager -def lifespan_startup(): - # Ensure logging is configured during startup - configure_logging() - yield - - -app = FastAPI() - - -@app.get("/") -def root(): - logger.info("This is an info message") - return {"Hello": "World"} diff --git a/.conflict-side-0/examples/simple_fastapi_app.py b/.conflict-side-0/examples/simple_fastapi_app.py deleted file mode 100644 index ff52251..0000000 --- a/.conflict-side-0/examples/simple_fastapi_app.py +++ /dev/null @@ -1,54 +0,0 @@ -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") diff --git a/.conflict-side-0/examples/single_file_app.py b/.conflict-side-0/examples/single_file_app.py deleted file mode 100644 index 4f4bb87..0000000 --- a/.conflict-side-0/examples/single_file_app.py +++ /dev/null @@ -1,114 +0,0 @@ -import time -from contextlib import asynccontextmanager -from typing import Annotated - -import anyio -import typer -from fast_depends import Depends -from fastapi import FastAPI - -from grelmicro.sync.leaderelection import LeaderElection -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.task import TaskManager - -backend = MemorySyncBackend() -task = TaskManager() - - -@asynccontextmanager -async def lifespan(app): - async with backend, task: - typer.echo("App started") - yield - typer.echo("App stopped") - - -app = FastAPI(lifespan=lifespan) - -leased_lock_10sec = Lock( - name="leased_lock_10sec", - lease_duration=10, - backend=backend, -) -leased_lock_5sec = Lock( - name="leased_lock_5sec", - lease_duration=5, - backend=backend, -) - -leader_election = LeaderElection(name="simple-leader", backend=backend) - -task.add_task(leader_election) - - -@task.interval(seconds=1) -def sync_func_with_no_param(): - typer.echo("sync_with_no_param") - - -@task.interval(seconds=2) -async def async_func_with_no_param(): - typer.echo("async_with_no_param") - - -def sync_dependency(): - return "sync_dependency" - - -@task.interval(seconds=3) -def sync_func_with_sync_dependency( - sync_dependency: Annotated[str, Depends(sync_dependency)], -): - typer.echo(sync_dependency) - - -async def async_dependency(): - yield "async_with_async_dependency" - - -@task.interval(seconds=4) -async def async_func_with_async_dependency( - async_dependency: Annotated[str, Depends(async_dependency)], -): - typer.echo(async_dependency) - - -@task.interval(seconds=15, sync=leased_lock_10sec) -def sync_func_with_leased_lock_10sec(): - typer.echo("sync_func_with_leased_lock_10sec") - time.sleep(9) - - -@task.interval(seconds=15, sync=leased_lock_10sec) -async def async_func_with_leased_lock_10sec(): - typer.echo("async_func_with_leased_lock_10sec") - await anyio.sleep(9) - - -@task.interval(seconds=15, sync=leased_lock_5sec) -def sync_func_with_sync_dependency_and_leased_lock_5sec( - sync_dependency: Annotated[str, Depends(sync_dependency)], -): - typer.echo(sync_dependency) - time.sleep(4) - - -@task.interval(seconds=15, sync=leased_lock_5sec) -async def async_func_with_async_dependency_and_leased_lock_5sec( - async_dependency: Annotated[str, Depends(async_dependency)], -): - typer.echo(async_dependency) - await anyio.sleep(4) - - -@task.interval(seconds=15, sync=leader_election) -def sync_func_with_leader_election(): - typer.echo("sync_func_with_leader_election") - time.sleep(30) - - -@task.interval(seconds=15, sync=leader_election) -async def async_func_with_leader_election(): - typer.echo("async_func_with_leader_election") - await anyio.sleep(30) diff --git a/.conflict-side-0/examples/sync/__init__.py b/.conflict-side-0/examples/sync/__init__.py deleted file mode 100644 index acd409a..0000000 --- a/.conflict-side-0/examples/sync/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Synchronization Primitives Examples.""" diff --git a/.conflict-side-0/examples/sync/leaderelection_anyio.py b/.conflict-side-0/examples/sync/leaderelection_anyio.py deleted file mode 100644 index 784f188..0000000 --- a/.conflict-side-0/examples/sync/leaderelection_anyio.py +++ /dev/null @@ -1,11 +0,0 @@ -from anyio import create_task_group, sleep_forever - -from grelmicro.sync.leaderelection import LeaderElection - -leader = LeaderElection("cluster_group") - - -async def main(): - async with create_task_group() as tg: - await tg.start(leader) - await sleep_forever() diff --git a/.conflict-side-0/examples/sync/leaderelection_task.py b/.conflict-side-0/examples/sync/leaderelection_task.py deleted file mode 100644 index 58fa926..0000000 --- a/.conflict-side-0/examples/sync/leaderelection_task.py +++ /dev/null @@ -1,6 +0,0 @@ -from grelmicro.sync import LeaderElection -from grelmicro.task import TaskManager - -leader = LeaderElection("cluster_group") -task = TaskManager() -task.add_task(leader) diff --git a/.conflict-side-0/examples/sync/lock.py b/.conflict-side-0/examples/sync/lock.py deleted file mode 100644 index 7f38fe6..0000000 --- a/.conflict-side-0/examples/sync/lock.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.sync import Lock - -lock = Lock("resource_name") - - -async def main(): - async with lock: - print("Protected resource accessed") diff --git a/.conflict-side-0/examples/sync/memory.py b/.conflict-side-0/examples/sync/memory.py deleted file mode 100644 index 7eefea9..0000000 --- a/.conflict-side-0/examples/sync/memory.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.memory import MemorySyncBackend - -backend = MemorySyncBackend() diff --git a/.conflict-side-0/examples/sync/postgres.py b/.conflict-side-0/examples/sync/postgres.py deleted file mode 100644 index ea8b8c3..0000000 --- a/.conflict-side-0/examples/sync/postgres.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.postgres import PostgresSyncBackend - -backend = PostgresSyncBackend("postgresql://user:password@localhost:5432/db") diff --git a/.conflict-side-0/examples/sync/redis.py b/.conflict-side-0/examples/sync/redis.py deleted file mode 100644 index 0625f5d..0000000 --- a/.conflict-side-0/examples/sync/redis.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.redis import RedisSyncBackend - -backend = RedisSyncBackend("redis://localhost:6379/0") diff --git a/.conflict-side-0/examples/task/__init__.py b/.conflict-side-0/examples/task/__init__.py deleted file mode 100644 index 20f7752..0000000 --- a/.conflict-side-0/examples/task/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Task Scheduler Examples.""" diff --git a/.conflict-side-0/examples/task/fastapi.py b/.conflict-side-0/examples/task/fastapi.py deleted file mode 100644 index 16aaa8e..0000000 --- a/.conflict-side-0/examples/task/fastapi.py +++ /dev/null @@ -1,16 +0,0 @@ -from contextlib import asynccontextmanager - -from fastapi import FastAPI - -from grelmicro.task import TaskManager - -task = TaskManager() - - -@asynccontextmanager -async def lifespan(app: FastAPI): - async with task: - yield - - -app = FastAPI(lifespan=lifespan) diff --git a/.conflict-side-0/examples/task/faststream.py b/.conflict-side-0/examples/task/faststream.py deleted file mode 100644 index 688c8d9..0000000 --- a/.conflict-side-0/examples/task/faststream.py +++ /dev/null @@ -1,18 +0,0 @@ -from contextlib import asynccontextmanager - -from faststream import ContextRepo, FastStream -from faststream.redis import RedisBroker - -from grelmicro.task import TaskManager - -task = TaskManager() - - -@asynccontextmanager -async def lifespan(context: ContextRepo): - async with task: - yield - - -broker = RedisBroker() -app = FastStream(broker, lifespan=lifespan) diff --git a/.conflict-side-0/examples/task/interval_manager.py b/.conflict-side-0/examples/task/interval_manager.py deleted file mode 100644 index 91beb2e..0000000 --- a/.conflict-side-0/examples/task/interval_manager.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.task import TaskManager - -task = TaskManager() - - -@task.interval(seconds=5) -async def my_task(): - print("Hello, World!") diff --git a/.conflict-side-0/examples/task/interval_router.py b/.conflict-side-0/examples/task/interval_router.py deleted file mode 100644 index f114ad7..0000000 --- a/.conflict-side-0/examples/task/interval_router.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.task import TaskRouter - -task = TaskRouter() - - -@task.interval(seconds=5) -async def my_task(): - print("Hello, World!") diff --git a/.conflict-side-0/examples/task/leaderelection.py b/.conflict-side-0/examples/task/leaderelection.py deleted file mode 100644 index ad12773..0000000 --- a/.conflict-side-0/examples/task/leaderelection.py +++ /dev/null @@ -1,12 +0,0 @@ -from grelmicro.sync import LeaderElection -from grelmicro.task import TaskManager - -leader = LeaderElection("my_task") -task = TaskManager() -task.add_task(leader) - - -@task.interval(seconds=5, sync=leader) -async def my_task(): - async with leader: - print("Hello, World!") diff --git a/.conflict-side-0/examples/task/lock.py b/.conflict-side-0/examples/task/lock.py deleted file mode 100644 index cdbf795..0000000 --- a/.conflict-side-0/examples/task/lock.py +++ /dev/null @@ -1,11 +0,0 @@ -from grelmicro.sync import Lock -from grelmicro.task import TaskManager - -lock = Lock("my_task") -task = TaskManager() - - -@task.interval(seconds=5, sync=lock) -async def my_task(): - async with lock: - print("Hello, World!") diff --git a/.conflict-side-0/examples/task/router.py b/.conflict-side-0/examples/task/router.py deleted file mode 100644 index 2b166aa..0000000 --- a/.conflict-side-0/examples/task/router.py +++ /dev/null @@ -1,15 +0,0 @@ -from grelmicro.task import TaskRouter - - -router = TaskRouter() - - -@router.interval(seconds=5) -async def my_task(): - print("Hello, World!") - - -from grelmicro.task.manager import TaskManager - -task = TaskManager() -task.include_router(router) diff --git a/.conflict-side-0/grelmicro/__init__.py b/.conflict-side-0/grelmicro/__init__.py deleted file mode 100644 index 7cc6d82..0000000 --- a/.conflict-side-0/grelmicro/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python.""" # noqa: E501 - -__version__ = "0.2.2" diff --git a/.conflict-side-0/grelmicro/errors.py b/.conflict-side-0/grelmicro/errors.py deleted file mode 100644 index 141f82e..0000000 --- a/.conflict-side-0/grelmicro/errors.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Grelmicro Errors.""" - -from typing import assert_never - -from pydantic import ValidationError - - -class GrelmicroError(Exception): - """Base Grelmicro error.""" - - -class OutOfContextError(GrelmicroError, RuntimeError): - """Outside Context Error. - - Raised when a method is called outside of the context manager. - """ - - def __init__(self, cls: object, method_name: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not call {cls.__class__.__name__}.{method_name} outside of the context manager" - ) - - -class DependencyNotFoundError(GrelmicroError, ImportError): - """Dependency Not Found Error.""" - - def __init__(self, *, module: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not import module {module}, try running 'pip install {module}'" - ) - - -class SettingsValidationError(GrelmicroError, ValueError): - """Settings Validation Error.""" - - def __init__(self, error: ValidationError | str) -> None: - """Initialize the error.""" - if isinstance(error, str): - details = error - elif isinstance(error, ValidationError): - details = "\n".join( - f"- {data['loc'][0]}: {data['msg']} [input={data['input']}]" - for data in error.errors() - ) - else: - assert_never(error) - - super().__init__( - f"Could not validate environment variables settings:\n{details}" - ) diff --git a/.conflict-side-0/grelmicro/logging/__init__.py b/.conflict-side-0/grelmicro/logging/__init__.py deleted file mode 100644 index 60d3d45..0000000 --- a/.conflict-side-0/grelmicro/logging/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Grelmicro Logging.""" - -from grelmicro.logging.loguru import configure_logging - -__all__ = ["configure_logging"] diff --git a/.conflict-side-0/grelmicro/logging/config.py b/.conflict-side-0/grelmicro/logging/config.py deleted file mode 100644 index a6301c1..0000000 --- a/.conflict-side-0/grelmicro/logging/config.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Logging Configuration.""" - -from enum import StrEnum -from typing import Self - -from pydantic import Field -from pydantic_settings import BaseSettings - - -class _CaseInsensitiveEnum(StrEnum): - @classmethod - def _missing_(cls, value: object) -> Self | None: - value = str(value).lower() - for member in cls: - if member.lower() == value: - return member - return None - - -class LoggingLevelType(_CaseInsensitiveEnum): - """Logging Level Enum.""" - - DEBUG = "DEBUG" - INFO = "INFO" - WARNING = "WARNING" - ERROR = "ERROR" - CRITICAL = "CRITICAL" - - -class LoggingFormatType(_CaseInsensitiveEnum): - """Logging Format Enum.""" - - JSON = "JSON" - TEXT = "TEXT" - - -class LoggingSettings(BaseSettings): - """Logging Settings.""" - - LOG_LEVEL: LoggingLevelType = LoggingLevelType.INFO - LOG_FORMAT: LoggingFormatType | str = Field( - LoggingFormatType.JSON, union_mode="left_to_right" - ) diff --git a/.conflict-side-0/grelmicro/logging/errors.py b/.conflict-side-0/grelmicro/logging/errors.py deleted file mode 100644 index 097006f..0000000 --- a/.conflict-side-0/grelmicro/logging/errors.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Grelmicro Logging Errors.""" - -from grelmicro.errors import SettingsValidationError - - -class LoggingSettingsValidationError(SettingsValidationError): - """Logging Settings Validation Error.""" diff --git a/.conflict-side-0/grelmicro/logging/loguru.py b/.conflict-side-0/grelmicro/logging/loguru.py deleted file mode 100644 index a94202c..0000000 --- a/.conflict-side-0/grelmicro/logging/loguru.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Loguru Logging.""" - -import json -import sys -from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, NotRequired - -from pydantic import ValidationError -from typing_extensions import TypedDict - -from grelmicro.errors import DependencyNotFoundError -from grelmicro.logging.config import LoggingFormatType, LoggingSettings -from grelmicro.logging.errors import LoggingSettingsValidationError - -if TYPE_CHECKING: - from loguru import FormatFunction, Record - -try: - import loguru -except ImportError: # pragma: no cover - loguru = None # type: ignore[assignment] - -try: - import orjson - - def _json_dumps(obj: Mapping[str, Any]) -> str: - return orjson.dumps(obj).decode("utf-8") -except ImportError: # pragma: no cover - import json - - _json_dumps = json.dumps - - -JSON_FORMAT = "{extra[serialized]}" -TEXT_FORMAT = ( - "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " - "{name}:{function}:{line} - {message}" -) - - -class JSONRecordDict(TypedDict): - """JSON log record representation. - - The time use a ISO 8601 string. - """ - - time: str - level: str - msg: str - logger: str | None - thread: str - ctx: NotRequired[dict[Any, Any]] - - -def json_patcher(record: "Record") -> None: - """Patch the serialized log record with `JSONRecordDict` representation.""" - json_record = JSONRecordDict( - time=record["time"].isoformat(), - level=record["level"].name, - thread=record["thread"].name, - logger=f'{record["name"]}:{record["function"]}:{record["line"]}', - msg=record["message"], - ) - - ctx = {k: v for k, v in record["extra"].items() if k != "serialized"} - exception = record["exception"] - - if exception and exception.type: - ctx["exception"] = f"{exception.type.__name__}: {exception.value!s}" - - if ctx: - json_record["ctx"] = ctx - - record["extra"]["serialized"] = _json_dumps(json_record) - - -def json_formatter(record: "Record") -> str: - """Format log record with `JSONRecordDict` representation. - - This function does not return the formatted record directly but provides the format to use when - writing to the sink. - """ - json_patcher(record) - return JSON_FORMAT + "\n" - - -def configure_logging() -> None: - """Configure logging with loguru. - - Simple twelve-factor app logging configuration that logs to stdout. - - The following environment variables are used: - - LOG_LEVEL: The log level to use (default: INFO). - - LOG_FORMAT: JSON | TEXT or any loguru template to format logged message (default: JSON). - - Raises: - MissingDependencyError: If the loguru module is not installed. - LoggingSettingsError: If the LOG_FORMAT or LOG_LEVEL environment variable is invalid - """ - if not loguru: - raise DependencyNotFoundError(module="loguru") - - try: - settings = LoggingSettings() - except ValidationError as error: - raise LoggingSettingsValidationError(error) from None - - logger = loguru.logger - log_format: str | FormatFunction = settings.LOG_FORMAT - - if log_format is LoggingFormatType.JSON: - log_format = json_formatter - elif log_format is LoggingFormatType.TEXT: - log_format = TEXT_FORMAT - - logger.remove() - logger.add( - sys.stdout, - level=settings.LOG_LEVEL, - format=log_format, - ) diff --git a/.conflict-side-0/grelmicro/py.typed b/.conflict-side-0/grelmicro/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/.conflict-side-0/grelmicro/sync/__init__.py b/.conflict-side-0/grelmicro/sync/__init__.py deleted file mode 100644 index 128d56c..0000000 --- a/.conflict-side-0/grelmicro/sync/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Grelmicro Synchronization Primitives.""" - -from grelmicro.sync.leaderelection import LeaderElection -from grelmicro.sync.lock import Lock - -__all__ = ["LeaderElection", "Lock"] diff --git a/.conflict-side-0/grelmicro/sync/_backends.py b/.conflict-side-0/grelmicro/sync/_backends.py deleted file mode 100644 index 66f4b9f..0000000 --- a/.conflict-side-0/grelmicro/sync/_backends.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Grelmicro Backend Registry. - -Contains loaded backends of each type to be used as default. - -Note: - For now, only lock backends are supported, but other backends may be added in the future. -""" - -from typing import Literal, NotRequired, TypedDict - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import BackendNotLoadedError - - -class LoadedBackendsDict(TypedDict): - """Loaded backends type.""" - - lock: NotRequired[SyncBackend] - - -loaded_backends: LoadedBackendsDict = {} - - -def get_sync_backend() -> SyncBackend: - """Get the lock backend.""" - backend: Literal["lock"] = "lock" - try: - return loaded_backends[backend] - except KeyError: - raise BackendNotLoadedError(backend) from None diff --git a/.conflict-side-0/grelmicro/sync/_base.py b/.conflict-side-0/grelmicro/sync/_base.py deleted file mode 100644 index a0e6fb0..0000000 --- a/.conflict-side-0/grelmicro/sync/_base.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Grelmicro Lock API.""" - -from types import TracebackType -from typing import Annotated, Protocol, Self -from uuid import UUID - -from pydantic import BaseModel, ConfigDict -from typing_extensions import Doc - -from grelmicro.sync.abc import Synchronization - - -class BaseLockConfig(BaseModel): - """Base Lock Config.""" - - model_config = ConfigDict(frozen=True, extra="forbid") - - name: Annotated[ - str, - Doc(""" - The name of the resource to lock. - """), - ] - worker: Annotated[ - str | UUID, - Doc(""" - The worker identity. - - By default, use a UUIDv1. - """), - ] - - -class BaseLock(Synchronization, Protocol): - """Base Lock Protocol.""" - - async def __aenter__(self) -> Self: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - """ - ... - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - ... - - @property - def config(self) -> BaseLockConfig: - """Return the config.""" - ... - - async def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - ... - - async def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - WouldBlock: If the lock cannot be acquired without blocking. - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - ... - - async def release(self) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - ... - - async def locked(self) -> bool: - """Check if the lock is currently held.""" - ... - - async def owned(self) -> bool: - """Check if the lock is currently held by the current token.""" - ... diff --git a/.conflict-side-0/grelmicro/sync/_utils.py b/.conflict-side-0/grelmicro/sync/_utils.py deleted file mode 100644 index 2ad5dda..0000000 --- a/.conflict-side-0/grelmicro/sync/_utils.py +++ /dev/null @@ -1,38 +0,0 @@ -from threading import get_ident -from uuid import NAMESPACE_DNS, UUID, uuid3 - -from anyio import get_current_task - - -def generate_worker_namespace(worker: str) -> UUID: - """Generate a worker UUIDv3 namespace. - - Generate a worker UUID using UUIDv3 with the DNS namespace. - """ - return uuid3(namespace=NAMESPACE_DNS, name=worker) - - -def generate_task_token(worker: UUID | str) -> str: - """Generate a task UUID. - - The worker namespace is generated using `generate_worker_uuid` if the worker is a string. - Generate a task UUID using UUIDv3 with the worker namespace and the async task ID. - """ - worker = ( - generate_worker_namespace(worker) if isinstance(worker, str) else worker - ) - task = str(get_current_task().id) - return str(uuid3(namespace=worker, name=task)) - - -def generate_thread_token(worker: UUID | str) -> str: - """Generate a thread UUID. - - The worker namespace is generated using `generate_worker_uuid` if the worker is a string. - Generate a thread UUID using UUIDv3 with the worker namespace and the current thread ID. - """ - worker = ( - generate_worker_namespace(worker) if isinstance(worker, str) else worker - ) - thread = str(get_ident()) - return str(uuid3(namespace=worker, name=thread)) diff --git a/.conflict-side-0/grelmicro/sync/abc.py b/.conflict-side-0/grelmicro/sync/abc.py deleted file mode 100644 index 507477c..0000000 --- a/.conflict-side-0/grelmicro/sync/abc.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Grelmicro Synchronization Abstract Base Classes and Protocols.""" - -from types import TracebackType -from typing import Protocol, Self, runtime_checkable - -from pydantic import PositiveFloat - - -class SyncBackend(Protocol): - """Synchronization Backend Protocol. - - This is the low level API for the distributed lock backend that is platform agnostic. - """ - - async def __aenter__(self) -> Self: - """Open the lock backend.""" - ... - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Close the lock backend.""" - ... - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock. - - Args: - name: The name of the lock. - token: The token to acquire the lock. - duration: The duration in seconds to hold the lock. - - Returns: - True if the lock is acquired, False if the lock is already acquired by another token. - - Raises: - Exception: Any exception can be raised if the lock cannot be acquired. - """ - ... - - async def release(self, *, name: str, token: str) -> bool: - """Release a lock. - - Args: - name: The name of the lock. - token: The token to release the lock. - - Returns: - True if the lock was released, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock cannot be released. - """ - ... - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired. - - Args: - name: The name of the lock. - - Returns: - True if the lock is acquired, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock status cannot be checked. - """ - ... - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned. - - Args: - name: The name of the lock. - token: The token to check. - - Returns: - True if the lock is owned by the token, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock status cannot be checked. - """ - ... - - -@runtime_checkable -class Synchronization(Protocol): - """Synchronization Primitive Protocol.""" - - async def __aenter__(self) -> Self: - """Enter the synchronization primitive.""" - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the synchronization primitive.""" - ... - - -Seconds = PositiveFloat diff --git a/.conflict-side-0/grelmicro/sync/errors.py b/.conflict-side-0/grelmicro/sync/errors.py deleted file mode 100644 index 6384e36..0000000 --- a/.conflict-side-0/grelmicro/sync/errors.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Grelmicro Synchronization Primitive Errors.""" - -from grelmicro.errors import SettingsValidationError - - -class SyncError(Exception): - """Synchronization Primitive Error. - - This the base class for all lock errors. - """ - - -class SyncBackendError(SyncError): - """Synchronization Backend Error.""" - - -class BackendNotLoadedError(SyncBackendError): - """Backend Not Loaded Error.""" - - def __init__(self, backend_name: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not load backend {backend_name}, try initializing one first" - ) - - -class LockAcquireError(SyncBackendError): - """Acquire Lock Error. - - This error is raised when an error on backend side occurs during lock acquisition. - """ - - def __init__(self, *, name: str, token: str) -> None: - """Initialize the error.""" - super().__init__(f"Failed to acquire lock: name={name}, token={token}") - - -class LockReleaseError(SyncBackendError): - """Lock Release Error. - - This error is raised when an error on backend side occurs during lock release. - """ - - def __init__( - self, *, name: str, token: str, reason: str | None = None - ) -> None: - """Initialize the error.""" - super().__init__( - f"Failed to release lock: name={name}, token={token}" - + (f", reason={reason}" if reason else ""), - ) - - -class LockNotOwnedError(LockReleaseError): - """Lock Not Owned Error during Release. - - This error is raised when an attempt is made to release a lock that is not owned, respectively - the token is different or the lock is already expired. - """ - - def __init__(self, *, name: str, token: str) -> None: - """Initialize the error.""" - super().__init__(name=name, token=token, reason="lock not owned") - - -class SyncSettingsValidationError(SyncError, SettingsValidationError): - """Synchronization Settings Validation Error.""" diff --git a/.conflict-side-0/grelmicro/sync/leaderelection.py b/.conflict-side-0/grelmicro/sync/leaderelection.py deleted file mode 100644 index 62ce539..0000000 --- a/.conflict-side-0/grelmicro/sync/leaderelection.py +++ /dev/null @@ -1,386 +0,0 @@ -"""Leader Election.""" - -from logging import getLogger -from time import monotonic -from types import TracebackType -from typing import TYPE_CHECKING, Annotated, Self -from uuid import UUID, uuid1 - -from anyio import ( - TASK_STATUS_IGNORED, - CancelScope, - Condition, - fail_after, - get_cancelled_exc_class, - move_on_after, - sleep, -) -from anyio.abc import TaskStatus -from pydantic import BaseModel, model_validator -from typing_extensions import Doc - -from grelmicro.sync._backends import get_sync_backend -from grelmicro.sync.abc import Seconds, SyncBackend, Synchronization -from grelmicro.task.abc import Task - -if TYPE_CHECKING: - from contextlib import AsyncExitStack - - from anyio.abc import TaskGroup - -logger = getLogger("grelmicro.leader_election") - - -class LeaderElectionConfig(BaseModel): - """Leader Election Config. - - Leader election based on a leased reentrant distributed lock. - """ - - name: Annotated[ - str, - Doc( - """ - The leader election lock name. - """, - ), - ] - worker: Annotated[ - str | UUID, - Doc( - """ - The worker identity used as lock token. - """, - ), - ] - lease_duration: Annotated[ - Seconds, - Doc( - """ - The lease duration in seconds. - """, - ), - ] = 15 - renew_deadline: Annotated[ - Seconds, - Doc( - """ - The renew deadline in seconds. - """, - ), - ] = 10 - retry_interval: Annotated[ - Seconds, - Doc( - """ - The retry interval in seconds. - """, - ), - ] = 2 - backend_timeout: Annotated[ - Seconds, - Doc( - """ - The backend timeout in seconds. - """, - ), - ] = 5 - error_interval: Annotated[ - Seconds, - Doc( - """ - The error interval in seconds. - """, - ), - ] = 30 - - @model_validator(mode="after") - def _validate(self) -> Self: - if self.renew_deadline >= self.lease_duration: - msg = "Renew deadline must be shorter than lease duration" - raise ValueError(msg) - if self.retry_interval >= self.renew_deadline: - msg = "Retry interval must be shorter than renew deadline" - raise ValueError(msg) - if self.backend_timeout >= self.renew_deadline: - msg = "Backend timeout must be shorter than renew deadline" - raise ValueError(msg) - return self - - -class LeaderElection(Synchronization, Task): - """Leader Election. - - The leader election is a synchronization primitive with the worker as scope. - It runs as a task to acquire or renew the distributed lock. - """ - - def __init__( - self, - name: Annotated[ - str, - Doc( - """ - The name of the resource representing the leader election. - - It will be used as the lock name so make sure it is unique on the distributed lock - backend. - """, - ), - ], - *, - backend: Annotated[ - SyncBackend | None, - Doc( - """ - The distributed lock backend used to acquire and release the lock. - - By default, it will use the lock backend registry to get the default lock backend. - """, - ), - ] = None, - worker: Annotated[ - str | UUID | None, - Doc( - """ - The worker identity. - - By default, use a UUIDv1 will be generated. - """, - ), - ] = None, - lease_duration: Annotated[ - Seconds, - Doc( - """ - The duration in seconds after the lock will be released if not renewed. - - If the worker becomes unavailable, the lock can only be acquired by an other worker - after it' has expired. - """, - ), - ] = 15, - renew_deadline: Annotated[ - Seconds, - Doc( - """ - The duration in seconds that the leader worker will try to acquire the lock before - giving up. - - Must be shorter than the lease duration. In case of multiple failures, the leader - worker will loose the lead to prevent split-brain scenarios and ensure that only one - worker is the leader at any time. - """, - ), - ] = 10, - retry_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between attempts to acquire or renew the lock. - - Must be shorter than the renew deadline. A shorter schedule enables faster leader - elections but may increase load on the distributed lock backend, while a longer - schedule reduces load but can delay new leader elections. - """, - ), - ] = 2, - backend_timeout: Annotated[ - Seconds, - Doc( - """ - The duration in seconds for waiting on backend for acquiring and releasing the lock. - - This value determines how long the system will wait before giving up the current - operation. - """, - ), - ] = 5, - error_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between logging error messages. - - If shorter than the retry interval, it will log every error. It is used to prevent - flooding the logs when the lock backend is unavailable. - """, - ), - ] = 30, - ) -> None: - """Initialize the leader election.""" - self.config = LeaderElectionConfig( - name=name, - worker=worker or uuid1(), - lease_duration=lease_duration, - renew_deadline=renew_deadline, - retry_interval=retry_interval, - backend_timeout=backend_timeout, - error_interval=error_interval, - ) - self.backend = backend or get_sync_backend() - - self._service_running = False - self._state_change_condition: Condition = Condition() - self._is_leader: bool = False - self._state_updated_at: float = monotonic() - self._error_logged_at: float | None = None - self._task_group: TaskGroup | None = None - self._exit_stack: AsyncExitStack | None = None - - async def __aenter__(self) -> Self: - """Wait for the leader with the context manager.""" - await self.wait_for_leader() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the context manager.""" - - @property - def name(self) -> str: - """Return the task name.""" - return self.config.name - - def is_running(self) -> bool: - """Check if the leader election task is running.""" - return self._service_running - - def is_leader(self) -> bool: - """Check if the current worker is the leader. - - To avoid a split-brain scenario, the leader considers itself as no longer leader if the - renew deadline is reached. - - Returns: - True if the current worker is the leader, False otherwise. - - """ - if not self._is_leader: - return False - return not self._is_renew_deadline_reached() - - async def wait_for_leader(self) -> None: - """Wait until the current worker is the leader.""" - while not self.is_leader(): - async with self._state_change_condition: - await self._state_change_condition.wait() - - async def wait_lose_leader(self) -> None: - """Wait until the current worker is no longer the leader.""" - while self.is_leader(): - with move_on_after(self._seconds_before_expiration_deadline()): - async with self._state_change_condition: - await self._state_change_condition.wait() - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run polling loop service to acquire or renew the distributed lock.""" - task_status.started() - if self._service_running: - logger.warning("Leader Election already running: %s", self.name) - return - self._service_running = True - logger.info("Leader Election started: %s", self.name) - try: - while True: - await self._try_acquire_or_renew() - await sleep(self.config.retry_interval) - except get_cancelled_exc_class(): - logger.info("Leader Election stopped: %s", self.name) - raise - except BaseException: - logger.exception("Leader Election crashed: %s", self.name) - raise - finally: - self._service_running = False - with CancelScope(shield=True): - await self._release() - - async def _update_state( - self, *, is_leader: bool, raison_if_no_more_leader: str - ) -> None: - """Update the state of the leader election.""" - self._state_updated_at = monotonic() - if is_leader is self._is_leader: - return # No change - - self._is_leader = is_leader - - if is_leader: - logger.info("Leader Election acquired leadership: %s", self.name) - else: - logger.warning( - "Leader Election lost leadership: %s (%s)", - self.name, - raison_if_no_more_leader, - ) - - async with self._state_change_condition: - self._state_change_condition.notify_all() - - async def _try_acquire_or_renew(self) -> None: - """Try to acquire leadership.""" - try: - with fail_after(self.config.backend_timeout): - is_leader = await self.backend.acquire( - name=self.name, - token=str(self.config.worker), - duration=self.config.lease_duration, - ) - except Exception: - if self._check_error_interval(): - logger.exception( - "Leader Election failed to acquire lock: %s", self.name - ) - if self._is_renew_deadline_reached(): - await self._update_state( - is_leader=False, - raison_if_no_more_leader="renew deadline reached", - ) - else: - await self._update_state( - is_leader=is_leader, - raison_if_no_more_leader="lock not acquired", - ) - - def _seconds_before_expiration_deadline(self) -> float: - return max( - self._state_updated_at + self.config.lease_duration - monotonic(), 0 - ) - - def _check_error_interval(self) -> bool: - """Check if the cooldown interval allows to log the error.""" - is_logging_allowed = ( - not self._error_logged_at - or (monotonic() - self._error_logged_at) - > self.config.error_interval - ) - self._error_logged_at = monotonic() - return is_logging_allowed - - def _is_renew_deadline_reached(self) -> bool: - return ( - monotonic() - self._state_updated_at - ) >= self.config.renew_deadline - - async def _release(self) -> None: - try: - with fail_after(self.config.backend_timeout): - if not ( - await self.backend.release( - name=self.config.name, token=str(self.config.worker) - ) - ): - logger.info( - "Leader Election lock already released: %s", self.name - ) - except Exception: - logger.exception( - "Leader Election failed to release lock: %s", self.name - ) diff --git a/.conflict-side-0/grelmicro/sync/lock.py b/.conflict-side-0/grelmicro/sync/lock.py deleted file mode 100644 index c87d08f..0000000 --- a/.conflict-side-0/grelmicro/sync/lock.py +++ /dev/null @@ -1,324 +0,0 @@ -"""Grelmicro Lock.""" - -from time import sleep as thread_sleep -from types import TracebackType -from typing import Annotated, Self -from uuid import UUID, uuid1 - -from anyio import WouldBlock, from_thread, sleep -from typing_extensions import Doc - -from grelmicro.sync._backends import get_sync_backend -from grelmicro.sync._base import BaseLock, BaseLockConfig -from grelmicro.sync._utils import generate_task_token, generate_thread_token -from grelmicro.sync.abc import Seconds, SyncBackend -from grelmicro.sync.errors import ( - LockAcquireError, - LockNotOwnedError, - LockReleaseError, - SyncBackendError, -) - - -class LockConfig(BaseLockConfig, frozen=True, extra="forbid"): - """Lock Config.""" - - lease_duration: Annotated[ - Seconds, - Doc( - """ - The lease duration in seconds for the lock. - """, - ), - ] - retry_interval: Annotated[ - Seconds, - Doc( - """ - The interval in seconds between attempts to acquire the lock. - """, - ), - ] - - -class Lock(BaseLock): - """Lock. - - This lock is a distributed lock that is used to acquire a resource across multiple workers. The - lock is acquired asynchronously and can be extended multiple times manually. The lock is - automatically released after a duration if not extended. - """ - - def __init__( - self, - name: Annotated[ - str, - Doc( - """ - The name of the resource to lock. - - It will be used as the lock name so make sure it is unique on the lock backend. - """, - ), - ], - *, - backend: Annotated[ - SyncBackend | None, - Doc(""" - The distributed lock backend used to acquire and release the lock. - - By default, it will use the lock backend registry to get the default lock backend. - """), - ] = None, - worker: Annotated[ - str | UUID | None, - Doc( - """ - The worker identity. - - By default, use a UUIDv1 will be generated. - """, - ), - ] = None, - lease_duration: Annotated[ - Seconds, - Doc( - """ - The duration in seconds for the lock to be held by default. - """, - ), - ] = 60, - retry_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between attempts to acquire the lock. - - Should be greater or equal than 0.1 to prevent flooding the lock backend. - """, - ), - ] = 0.1, - ) -> None: - """Initialize the lock.""" - self._config: LockConfig = LockConfig( - name=name, - worker=worker or uuid1(), - lease_duration=lease_duration, - retry_interval=retry_interval, - ) - self.backend = backend or get_sync_backend() - self._from_thread: ThreadLockAdapter | None = None - - async def __aenter__(self) -> Self: - """Acquire the lock with the async context manager.""" - await self.acquire() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock with the async context manager. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - await self.release() - - @property - def config(self) -> LockConfig: - """Return the lock config.""" - return self._config - - @property - def from_thread(self) -> "ThreadLockAdapter": - """Return the lock adapter for worker thread.""" - if self._from_thread is None: - self._from_thread = ThreadLockAdapter(lock=self) - return self._from_thread - - async def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - while not await self.do_acquire(token=token): # noqa: ASYNC110 // Polling is intentional - await sleep(self._config.retry_interval) - - async def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - WouldBlock: If the lock cannot be acquired without blocking. - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - if not await self.do_acquire(token=token): - msg = f"Lock not acquired: name={self._config.name}, token={token}" - raise WouldBlock(msg) - - async def release(self) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - if not await self.do_release(token): - raise LockNotOwnedError(name=self._config.name, token=token) - - async def locked(self) -> bool: - """Check if the lock is acquired. - - Raise: - SyncBackendError: If the lock cannot be checked due to an error on the backend. - """ - try: - return await self.backend.locked(name=self._config.name) - except Exception as exc: - msg = "Failed to check if the lock is acquired" - raise SyncBackendError(msg) from exc - - async def owned(self) -> bool: - """Check if the lock is owned by the current token. - - Raise: - SyncBackendError: If the lock cannot be checked due to an error on the backend. - """ - return await self.do_owned(generate_task_token(self._config.worker)) - - async def do_acquire(self, token: str) -> bool: - """Acquire the lock. - - This method should not be called directly. Use `acquire` instead. - - Returns: - bool: True if the lock was acquired, False if the lock was not acquired. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - """ - try: - return await self.backend.acquire( - name=self._config.name, - token=token, - duration=self._config.lease_duration, - ) - except Exception as exc: - raise LockAcquireError(name=self._config.name, token=token) from exc - - async def do_release(self, token: str) -> bool: - """Release the lock. - - This method should not be called directly. Use `release` instead. - - Returns: - bool: True if the lock was released, False otherwise. - - Raises: - LockReleaseError: Cannot release the lock due to backend error. - """ - try: - return await self.backend.release( - name=self._config.name, token=token - ) - except Exception as exc: - raise LockReleaseError(name=self._config.name, token=token) from exc - - async def do_owned(self, token: str) -> bool: - """Check if the lock is owned by the current token. - - This method should not be called directly. Use `owned` instead. - - Returns: - bool: True if the lock is owned by the current token, False otherwise. - - Raises: - SyncBackendError: Cannot check if the lock is owned due to backend error. - """ - try: - return await self.backend.owned(name=self._config.name, token=token) - except Exception as exc: - msg = "Failed to check if the lock is owned" - raise SyncBackendError(msg) from exc - - -class ThreadLockAdapter: - """Lock Adapter for Worker Thread.""" - - def __init__(self, lock: Lock) -> None: - """Initialize the lock adapter.""" - self._lock = lock - - def __enter__(self) -> Self: - """Acquire the lock with the context manager.""" - self.acquire() - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock with the context manager.""" - self.release() - - def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: Cannot acquire the lock due to backend error. - - """ - token = generate_thread_token(self._lock.config.worker) - retry_interval = self._lock.config.retry_interval - while not from_thread.run(self._lock.do_acquire, token): - thread_sleep(retry_interval) - - def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - LockAcquireError: Cannot acquire the lock due to backend error. - WouldBlock: If the lock cannot be acquired without blocking. - - """ - token = generate_thread_token(self._lock.config.worker) - if not from_thread.run(self._lock.do_acquire, token): - msg = f"Lock not acquired: name={self._lock.config.name}, token={token}" - raise WouldBlock(msg) - - def release(self) -> None: - """Release the lock. - - Raises: - ReleaseSyncBackendError: Cannot release the lock due to backend error. - LockNotOwnedError: If the lock is not currently held. - - """ - token = generate_thread_token(self._lock.config.worker) - if not from_thread.run(self._lock.do_release, token): - raise LockNotOwnedError(name=self._lock.config.name, token=token) - - def locked(self) -> bool: - """Return True if the lock is currently held.""" - return from_thread.run(self._lock.locked) - - def owned(self) -> bool: - """Return True if the lock is currently held by the current worker thread.""" - return from_thread.run( - self._lock.do_owned, generate_thread_token(self._lock.config.worker) - ) diff --git a/.conflict-side-0/grelmicro/sync/memory.py b/.conflict-side-0/grelmicro/sync/memory.py deleted file mode 100644 index 9746c59..0000000 --- a/.conflict-side-0/grelmicro/sync/memory.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Memory Synchronization Backend.""" - -from time import monotonic -from types import TracebackType -from typing import Annotated, Self - -from typing_extensions import Doc - -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend - - -class MemorySyncBackend(SyncBackend): - """Memory Synchronization Backend. - - This is not a backend with a real distributed lock. It is a local lock that can be used for - testing purposes or for locking operations that are executed in the same AnyIO event loop. - """ - - def __init__( - self, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - ) -> None: - """Initialize the lock backend.""" - self._locks: dict[str, tuple[str | None, float]] = {} - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Enter the lock backend.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Exit the lock backend.""" - self._locks.clear() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - if ( - current_token is None - or current_token == token - or expire_at < monotonic() - ): - self._locks[name] = (token, monotonic() + duration) - return True - return False - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - if current_token == token and expire_at >= monotonic(): - del self._locks[name] - return True - if current_token and expire_at < monotonic(): - del self._locks[name] - return False - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - return current_token is not None and expire_at >= monotonic() - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - return current_token == token and expire_at >= monotonic() diff --git a/.conflict-side-0/grelmicro/sync/postgres.py b/.conflict-side-0/grelmicro/sync/postgres.py deleted file mode 100644 index 35b32a1..0000000 --- a/.conflict-side-0/grelmicro/sync/postgres.py +++ /dev/null @@ -1,198 +0,0 @@ -"""PostgreSQL Synchronization Backend.""" - -from types import TracebackType -from typing import Annotated, Self - -from asyncpg import Pool, create_pool -from pydantic import PostgresDsn -from pydantic_core import MultiHostUrl, ValidationError -from pydantic_settings import BaseSettings -from typing_extensions import Doc - -from grelmicro.errors import OutOfContextError -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import SyncSettingsValidationError - - -class _PostgresSettings(BaseSettings): - POSTGRES_HOST: str | None = None - POSTGRES_PORT: int = 5432 - POSTGRES_DB: str | None = None - POSTGRES_USER: str | None = None - POSTGRES_PASSWORD: str | None = None - POSTGRES_URL: PostgresDsn | None = None - - def url(self) -> str: - """Generate the Postgres URL from the parts.""" - if self.POSTGRES_URL: - return self.POSTGRES_URL.unicode_string() - - if all( - ( - self.POSTGRES_HOST, - self.POSTGRES_DB, - self.POSTGRES_USER, - self.POSTGRES_PASSWORD, - ) - ): - return MultiHostUrl.build( - scheme="postgresql", - username=self.POSTGRES_USER, - password=self.POSTGRES_PASSWORD, - host=self.POSTGRES_HOST, - port=self.POSTGRES_PORT, - path=self.POSTGRES_DB, - ).unicode_string() - - msg = ( - "Either POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and " - "POSTGRES_PASSWORD must be set" - ) - raise SyncSettingsValidationError(msg) - - -class PostgresSyncBackend(SyncBackend): - """PostgreSQL Synchronization Backend.""" - - _SQL_CREATE_TABLE_IF_NOT_EXISTS = """ - CREATE TABLE IF NOT EXISTS {table_name} ( - name TEXT PRIMARY KEY, - token TEXT NOT NULL, - expire_at TIMESTAMP NOT NULL - ); - """ - - _SQL_ACQUIRE_OR_EXTEND = """ - INSERT INTO {table_name} (name, token, expire_at) - VALUES ($1, $2, NOW() + make_interval(secs => $3)) - ON CONFLICT (name) DO UPDATE - SET token = EXCLUDED.token, expire_at = EXCLUDED.expire_at - WHERE {table_name}.token = EXCLUDED.token OR {table_name}.expire_at < NOW() - RETURNING 1; - """ - - _SQL_RELEASE = """ - DELETE FROM {table_name} - WHERE name = $1 AND token = $2 AND expire_at >= NOW() - RETURNING 1; - """ - - _SQL_RELEASE_ALL_EXPIRED = """ - DELETE FROM {table_name} - WHERE expire_at < NOW(); - """ - - _SQL_LOCKED = """ - SELECT 1 FROM {table_name} - WHERE name = $1 AND expire_at >= NOW(); - """ - - _SQL_OWNED = """ - SELECT 1 FROM {table_name} - WHERE name = $1 AND token = $2 AND expire_at >= NOW(); - """ - - def __init__( - self, - url: Annotated[ - PostgresDsn | str | None, - Doc(""" - The Postgres database URL. - - If not provided, the URL will be taken from the environment variables POSTGRES_URL - or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD. - """), - ] = None, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - table_name: Annotated[ - str, Doc("The table name to store the locks.") - ] = "locks", - ) -> None: - """Initialize the lock backend.""" - if not table_name.isidentifier(): - msg = f"Table name '{table_name}' is not a valid identifier" - raise ValueError(msg) - - try: - self._url = url or _PostgresSettings().url() - except ValidationError as error: - raise SyncSettingsValidationError(error) from None - - self._table_name = table_name - self._acquire_sql = self._SQL_ACQUIRE_OR_EXTEND.format( - table_name=table_name - ) - self._release_sql = self._SQL_RELEASE.format(table_name=table_name) - self._pool: Pool | None = None - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Enter the lock backend.""" - self._pool = await create_pool(str(self._url)) - await self._pool.execute( - self._SQL_CREATE_TABLE_IF_NOT_EXISTS.format( - table_name=self._table_name - ), - ) - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Exit the lock backend.""" - if self._pool: - await self._pool.execute( - self._SQL_RELEASE_ALL_EXPIRED.format( - table_name=self._table_name - ), - ) - await self._pool.close() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire a lock.""" - if not self._pool: - raise OutOfContextError(self, "acquire") - - return bool( - await self._pool.fetchval(self._acquire_sql, name, token, duration) - ) - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - if not self._pool: - raise OutOfContextError(self, "release") - return bool(await self._pool.fetchval(self._release_sql, name, token)) - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - if not self._pool: - raise OutOfContextError(self, "locked") - return bool( - await self._pool.fetchval( - self._SQL_LOCKED.format(table_name=self._table_name), - name, - ), - ) - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - if not self._pool: - raise OutOfContextError(self, "owned") - return bool( - await self._pool.fetchval( - self._SQL_OWNED.format(table_name=self._table_name), - name, - token, - ), - ) diff --git a/.conflict-side-0/grelmicro/sync/redis.py b/.conflict-side-0/grelmicro/sync/redis.py deleted file mode 100644 index 73090c8..0000000 --- a/.conflict-side-0/grelmicro/sync/redis.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Redis Synchronization Backend.""" - -from types import TracebackType -from typing import Annotated, Self - -from pydantic import RedisDsn, ValidationError -from pydantic_core import Url -from pydantic_settings import BaseSettings -from redis.asyncio.client import Redis -from typing_extensions import Doc - -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import SyncSettingsValidationError - - -class _RedisSettings(BaseSettings): - """Redis settings from the environment variables.""" - - REDIS_HOST: str | None = None - REDIS_PORT: int = 6379 - REDIS_DB: int = 0 - REDIS_PASSWORD: str | None = None - REDIS_URL: RedisDsn | None = None - - -def _get_redis_url() -> str: - """Get the Redis URL from the environment variables. - - Raises: - SyncSettingsValidationError: If the URL or host is not set. - """ - try: - settings = _RedisSettings() - except ValidationError as error: - raise SyncSettingsValidationError(error) from None - - if settings.REDIS_URL and not settings.REDIS_HOST: - return settings.REDIS_URL.unicode_string() - - if settings.REDIS_HOST and not settings.REDIS_URL: - return Url.build( - scheme="redis", - host=settings.REDIS_HOST, - port=settings.REDIS_PORT, - path=str(settings.REDIS_DB), - password=settings.REDIS_PASSWORD, - ).unicode_string() - - msg = "Either REDIS_URL or REDIS_HOST must be set" - raise SyncSettingsValidationError(msg) - - -class RedisSyncBackend(SyncBackend): - """Redis Synchronization Backend.""" - - _LUA_ACQUIRE_OR_EXTEND = """ - local token = redis.call('get', KEYS[1]) - if not token then - redis.call('set', KEYS[1], ARGV[1], 'px', ARGV[2]) - return 1 - end - if token == ARGV[1] then - redis.call('pexpire', KEYS[1], ARGV[2]) - return 1 - end - return 0 - """ - _LUA_RELEASE = """ - local token = redis.call('get', KEYS[1]) - if not token or token ~= ARGV[1] then - return 0 - end - redis.call('del', KEYS[1]) - return 1 - """ - - def __init__( - self, - url: Annotated[ - RedisDsn | str | None, - Doc(""" - The Redis URL. - - If not provided, the URL will be taken from the environment variables REDIS_URL - or REDIS_HOST, REDIS_PORT, REDIS_DB, and REDIS_PASSWORD. - """), - ] = None, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - ) -> None: - """Initialize the lock backend.""" - self._url = url or _get_redis_url() - self._redis: Redis = Redis.from_url(str(self._url)) - self._lua_release = self._redis.register_script(self._LUA_RELEASE) - self._lua_acquire = self._redis.register_script( - self._LUA_ACQUIRE_OR_EXTEND - ) - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Open the lock backend.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Close the lock backend.""" - await self._redis.aclose() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock.""" - return bool( - await self._lua_acquire( - keys=[name], - args=[token, int(duration * 1000)], - client=self._redis, - ) - ) - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - return bool( - await self._lua_release( - keys=[name], args=[token], client=self._redis - ) - ) - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - return bool(await self._redis.get(name)) - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - return bool( - (await self._redis.get(name)) == token.encode() - ) # redis returns bytes diff --git a/.conflict-side-0/grelmicro/task/__init__.py b/.conflict-side-0/grelmicro/task/__init__.py deleted file mode 100644 index 374bf08..0000000 --- a/.conflict-side-0/grelmicro/task/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Grelmicro Task Scheduler.""" - -from grelmicro.task.manager import TaskManager -from grelmicro.task.router import TaskRouter - -__all__ = ["TaskManager", "TaskRouter"] diff --git a/.conflict-side-0/grelmicro/task/_interval.py b/.conflict-side-0/grelmicro/task/_interval.py deleted file mode 100644 index f66c2f2..0000000 --- a/.conflict-side-0/grelmicro/task/_interval.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Interval Task.""" - -from collections.abc import Awaitable, Callable -from contextlib import nullcontext -from functools import partial -from inspect import iscoroutinefunction -from logging import getLogger -from typing import Any - -from anyio import TASK_STATUS_IGNORED, sleep, to_thread -from anyio.abc import TaskStatus -from fast_depends import inject - -from grelmicro.sync.abc import Synchronization -from grelmicro.task._utils import validate_and_generate_reference -from grelmicro.task.abc import Task - -logger = getLogger("grelmicro.task") - - -class IntervalTask(Task): - """Interval Task. - - Use the `TaskManager.interval()` or `SchedulerRouter.interval()` decorator instead - of creating IntervalTask objects directly. - """ - - def __init__( - self, - *, - function: Callable[..., Any], - name: str | None = None, - interval: float, - sync: Synchronization | None = None, - ) -> None: - """Initialize the IntervalTask. - - Raises: - FunctionNotSupportedError: If the function is not supported. - ValueError: If internal is less than or equal to 0. - """ - if interval <= 0: - msg = "Interval must be greater than 0" - raise ValueError(msg) - - alt_name = validate_and_generate_reference(function) - self._name = name or alt_name - self._interval = interval - self._async_function = self._prepare_async_function(function) - self._sync = sync if sync else nullcontext() - - @property - def name(self) -> str: - """Return the lock name.""" - return self._name - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run the repeated task loop.""" - logger.info( - "Task started (interval: %ss): %s", self._interval, self.name - ) - task_status.started() - try: - while True: - try: - async with self._sync: - try: - await self._async_function() - except Exception: - logger.exception( - "Task execution error: %s", self.name - ) - except Exception: - logger.exception( - "Task synchronization error: %s", self.name - ) - await sleep(self._interval) - finally: - logger.info("Task stopped: %s", self.name) - - def _prepare_async_function( - self, function: Callable[..., Any] - ) -> Callable[..., Awaitable[Any]]: - """Prepare the function with lock and ensure async function.""" - function = inject(function) - return ( - function - if iscoroutinefunction(function) - else partial(to_thread.run_sync, function) - ) diff --git a/.conflict-side-0/grelmicro/task/_utils.py b/.conflict-side-0/grelmicro/task/_utils.py deleted file mode 100644 index 7cfec3f..0000000 --- a/.conflict-side-0/grelmicro/task/_utils.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Task Utilities.""" - -from collections.abc import Callable -from functools import partial -from inspect import ismethod -from typing import Any - -from grelmicro.task.errors import FunctionTypeError - - -def validate_and_generate_reference(function: Callable[..., Any]) -> str: - """Generate a task name from the given function. - - This implementation is inspirated by the APScheduler project under MIT License. - Original source: https://github.com/agronholm/apscheduler/blob/master/src/apscheduler/_marshalling.py - - Raises: - FunctionNotSupportedError: If function is not supported. - - """ - if isinstance(function, partial): - ref = "partial()" - raise FunctionTypeError(ref) - - if ismethod(function): - ref = "method" - raise FunctionTypeError(ref) - - if not hasattr(function, "__module__") or not hasattr( - function, "__qualname__" - ): - ref = "callable without __module__ or __qualname__ attribute" - raise FunctionTypeError(ref) - - if "" in function.__qualname__: - ref = "lambda" - raise FunctionTypeError(ref) - - if "" in function.__qualname__: - ref = "nested function" - raise FunctionTypeError(ref) - - return f"{function.__module__}:{function.__qualname__}" diff --git a/.conflict-side-0/grelmicro/task/abc.py b/.conflict-side-0/grelmicro/task/abc.py deleted file mode 100644 index d4e7cf3..0000000 --- a/.conflict-side-0/grelmicro/task/abc.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Grelmicro Task Synchronization Abstract Base Classes and Protocols.""" - -from typing import Protocol - -from anyio import TASK_STATUS_IGNORED -from anyio.abc import TaskStatus -from typing_extensions import runtime_checkable - - -@runtime_checkable -class Task(Protocol): - """Task Protocol. - - A task that runs in background in the async event loop. - """ - - @property - def name(self) -> str: - """Name to uniquely identify the task.""" - ... - - async def __call__( - self, - *, - task_status: TaskStatus[None] = TASK_STATUS_IGNORED, - ) -> None: - """Run the task. - - This is the entry point of the task to be run in the async event loop. - """ - ... diff --git a/.conflict-side-0/grelmicro/task/errors.py b/.conflict-side-0/grelmicro/task/errors.py deleted file mode 100644 index a788f61..0000000 --- a/.conflict-side-0/grelmicro/task/errors.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Grelmicro Task Scheduler Errors.""" - -from grelmicro.errors import GrelmicroError - - -class TaskError(GrelmicroError): - """Base Grelmicro Task error.""" - - -class FunctionTypeError(TaskError, TypeError): - """Function Type Error.""" - - def __init__(self, reference: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not use function {reference}, " - "try declaring 'def' or 'async def' directly in the module" - ) - - -class TaskAddOperationError(TaskError, RuntimeError): - """Task Add Operation Error.""" - - def __init__(self) -> None: - """Initialize the error.""" - super().__init__( - "Could not add the task, try calling 'add_task' and 'include_router' before starting" - ) diff --git a/.conflict-side-0/grelmicro/task/manager.py b/.conflict-side-0/grelmicro/task/manager.py deleted file mode 100644 index 5432145..0000000 --- a/.conflict-side-0/grelmicro/task/manager.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Grelmicro Task Manager.""" - -from contextlib import AsyncExitStack -from logging import getLogger -from types import TracebackType -from typing import TYPE_CHECKING, Annotated, Self - -from anyio import create_task_group -from typing_extensions import Doc - -from grelmicro.errors import OutOfContextError -from grelmicro.task.abc import Task -from grelmicro.task.errors import TaskAddOperationError -from grelmicro.task.router import TaskRouter - -if TYPE_CHECKING: - from anyio.abc import TaskGroup - -logger = getLogger("grelmicro.task") - - -class TaskManager(TaskRouter): - """Task Manager. - - `TaskManager` class, the main entrypoint to manage scheduled tasks. - """ - - def __init__( - self, - *, - auto_start: Annotated[ - bool, - Doc( - """ - Automatically start all tasks. - """, - ), - ] = True, - tasks: Annotated[ - list[Task] | None, - Doc( - """ - A list of tasks to be started. - """, - ), - ] = None, - ) -> None: - """Initialize the task manager.""" - TaskRouter.__init__(self, tasks=tasks) - - self._auto_start = auto_start - self._task_group: TaskGroup | None = None - - async def __aenter__(self) -> Self: - """Enter the context manager.""" - self._exit_stack = AsyncExitStack() - await self._exit_stack.__aenter__() - self._task_group = await self._exit_stack.enter_async_context( - create_task_group(), - ) - if self._auto_start: - await self.start() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the context manager.""" - if not self._task_group or not self._exit_stack: - raise OutOfContextError(self, "__aexit__") - self._task_group.cancel_scope.cancel() - return await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb) - - async def start(self) -> None: - """Start all tasks manually.""" - if not self._task_group: - raise OutOfContextError(self, "start") - - if self._started: - raise TaskAddOperationError - - self.do_mark_as_started() - - for task in self.tasks: - await self._task_group.start(task.__call__) - logger.debug("%s scheduled tasks started", len(self._tasks)) diff --git a/.conflict-side-0/grelmicro/task/router.py b/.conflict-side-0/grelmicro/task/router.py deleted file mode 100644 index 16b240d..0000000 --- a/.conflict-side-0/grelmicro/task/router.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Grelmicro Task Router.""" - -from collections.abc import Awaitable, Callable -from typing import Annotated, Any - -from typing_extensions import Doc - -from grelmicro.sync.abc import Synchronization -from grelmicro.task.abc import Task -from grelmicro.task.errors import TaskAddOperationError - - -class TaskRouter: - """Task Router. - - `TaskRouter` class, used to group task schedules, for example to structure an app in - multiple files. It would then included in the `TaskManager`, or in another - `TaskRouter`. - """ - - def __init__( - self, - *, - tasks: Annotated[ - list[Task] | None, - Doc( - """ - A list of schedules or scheduled tasks to be scheduled. - """, - ), - ] = None, - ) -> None: - """Initialize the task router.""" - self._started = False - self._tasks: list[Task] = tasks or [] - self._routers: list[TaskRouter] = [] - - @property - def tasks(self) -> list[Task]: - """List of scheduled tasks.""" - return self._tasks + [ - task for router in self._routers for task in router.tasks - ] - - def add_task(self, task: Task) -> None: - """Add a task to the scheduler.""" - if self._started: - raise TaskAddOperationError - - self._tasks.append(task) - - def interval( - self, - *, - seconds: Annotated[ - float, - Doc( - """ - The duration in seconds between each task run. - - Accuracy is not guaranteed and may vary with system load. Consider the - execution time of the task when setting the interval. - """, - ), - ], - name: Annotated[ - str | None, - Doc( - """ - The name of the task. - - If None, a name will be generated automatically from the function. - """, - ), - ] = None, - sync: Annotated[ - Synchronization | None, - Doc( - """ - The synchronization primitive to use for the task. - - You can use a `LeasedLock` or a `LeaderElection`, for example. If None, - no synchronization is used and the task will run on all workers. - """, - ), - ] = None, - ) -> Callable[ - [Callable[..., Any | Awaitable[Any]]], - Callable[..., Any | Awaitable[Any]], - ]: - """Decorate function to add it to the task scheduler. - - Raises: - TaskNameGenerationError: If the task name generation fails. - """ - from grelmicro.task._interval import IntervalTask - - def decorator( - function: Callable[[], None | Awaitable[None]], - ) -> Callable[[], None | Awaitable[None]]: - self.add_task( - IntervalTask( - name=name, - function=function, - interval=seconds, - sync=sync, - ), - ) - return function - - return decorator - - def include_router(self, router: "TaskRouter") -> None: - """Include another router in this router.""" - if self._started: - raise TaskAddOperationError - - self._routers.append(router) - - def started(self) -> bool: - """Check if the task manager has started.""" - return self._started - - def do_mark_as_started(self) -> None: - """Mark the task manager as started. - - Do not call this method directly. It is called by the task manager when the task - manager is started. - """ - self._started = True - for router in self._routers: - router.do_mark_as_started() diff --git a/.conflict-side-0/mkdocs.yml b/.conflict-side-0/mkdocs.yml deleted file mode 100644 index 0b08e9f..0000000 --- a/.conflict-side-0/mkdocs.yml +++ /dev/null @@ -1,47 +0,0 @@ -site_name: Grelmicro -site_description: Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. -site_url: https://grelmicro.grel.info -theme: - name: material - palette: - primary: green - accent: light green - font: - text: 'Roboto' - code: 'Roboto Mono' - features: - - content.tabs.link - - content.code.copy - - content.code.select - - content.tooltips - - navigation.indexes - - navigation.instant - - navigation.instant.prefetch - - navigation.instant.progress - - navigation.top - - navigation.tracking - -repo_name: grelinfo/grelmicro -repo_url: https://github.com/grelinfo/grelmicro - -validation: - omitted_files: warn - absolute_links: warn - unrecognized_links: warn - -nav: -- Grelmicro: index.md -- User Guide: - - logging.md - - sync.md - - task.md - -markdown_extensions: - - admonition - - mdx_include: - base_path: docs - - pymdownx.highlight - - pymdownx.superfences - - pymdownx.inlinehilite - - pymdownx.tabbed: - alternate_style: true diff --git a/.conflict-side-0/pyproject.toml b/.conflict-side-0/pyproject.toml deleted file mode 100644 index 9bcca87..0000000 --- a/.conflict-side-0/pyproject.toml +++ /dev/null @@ -1,174 +0,0 @@ -[project] -name = "grelmicro" -description = "Grelmicro is a lightweight framework/toolkit for building async microservices in Python" -license = "MIT" -authors = [{ name = "Loïc Gremaud", email = "grelinfo@gmail.com"}] -readme = "README.md" - -classifiers = [ - "Intended Audience :: Information Technology", - "Intended Audience :: System Administrators", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python", - "Topic :: Internet", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development", - "Typing :: Typed", - "Development Status :: 1 - Planning", - "Environment :: Web Environment", - "Framework :: AsyncIO", - "Framework :: FastAPI", - "Framework :: Pydantic", - "Framework :: Pydantic :: 2", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", -] -dynamic = ["version"] - -requires-python = ">=3.11" - -dependencies = [ - "anyio>=4.0.0", - "pydantic>=2.5.0", - "fast-depends>=2.0.0", - "pydantic-settings>=2.5.0", -] - -[project.urls] - -Repository = "https://github.com/grelinfo/grelmicro.git" -Issues = "https://github.com/grelinfo/grelmicro/issues" - -[project.optional-dependencies] -standard = [ - "loguru>=0.7.2", - "orjson>=3.10.11", -] -postgres = [ - "asyncpg>=0.30.0", -] -redis = [ - "redis>=5.0.0", -] - -[dependency-groups] -dev = [ - "pytest-cov>=6.0.0", - "pytest>=8.0.0", - "mypy>=1.12.0", - "ruff>=0.7.4", - "testcontainers[postgres,redis]>=4.8.2", - "pytest-timeout>=2.3.1", - "pytest-mock>=3.14.0", - "pytest-randomly>=3.16.0", - "pre-commit>=4.0.1", - "fastapi>=0.115.5", - "fastapi-cli>=0.0.5", - "mdx-include>=1.4.2", - "faststream>=0.5.30", - "hatch>=1.13.0", -] -docs = [ - "mkdocs-material>=9.5.44", - "pygments>=2.18.0", - "pymdown-extensions>=10.12", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build] -skip-excluded-dirs = true -exclude = ["/tests", "/docs", "/examples"] - -[tool.hatch.version] -path = "grelmicro/__init__.py" - -[tool.ruff] -target-version = "py311" -line-length = 80 - -[tool.ruff.lint] -select = ["ALL"] -ignore = ["COM812", "ISC001"] # Ignore rules conflicting with the formatter. - -[tool.ruff.lint.extend-per-file-ignores] -"examples/*" = [ - "ARG001", - "ANN001", - "ANN201", - "D103", - "D100", - "INP001", - "T201", -] -"examples/logging/basic.py" = ["EM101", "TRY"] -"examples/task/router.py" = ["I001", "E402"] -"tests/*" = [ - "S101", - "SLF001" -] - -[tool.ruff.lint.pycodestyle] -max-line-length = 100 # reports only line that exceed 100 characters. - -[tool.ruff.lint.pydocstyle] -convention = "pep257" - -[tool.ruff.lint.pylint] -max-args = 10 - -[tool.mypy] -scripts_are_modules = true -plugins = [ - "pydantic.mypy" -] -follow_imports = "silent" -warn_redundant_casts = true -warn_unused_ignores = true -disallow_any_generics = true -check_untyped_defs = true -no_implicit_reexport = true -disallow_untyped_defs = true - -[[tool.mypy.overrides]] -module = ["asyncpg", "testcontainers.*"] -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = [ - "examples.*" -] -disallow_untyped_defs = false - - -[tool.pytest.ini_options] -addopts = """ - --cov=grelmicro - --cov-report term:skip-covered - --cov-report xml:cov.xml - --strict-config - --strict-markers - -m "not integration" -""" -markers = """ - integration: mark a test as an integration test (disabled by default). -""" - -testpaths = "tests" - -[tool.coverage.report] -sort = "-Cover" -exclude_also = [ - "if TYPE_CHECKING:", - "class .*\\bProtocol\\):", - "assert_never\\(.*\\)", -] diff --git a/.conflict-side-0/tests/__init__.py b/.conflict-side-0/tests/__init__.py deleted file mode 100644 index adc28b2..0000000 --- a/.conflict-side-0/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Tests.""" diff --git a/.conflict-side-0/tests/conftest.py b/.conflict-side-0/tests/conftest.py deleted file mode 100644 index 916c148..0000000 --- a/.conflict-side-0/tests/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Grelmicro Test Config.""" - -import pytest - - -@pytest.fixture -def anyio_backend() -> str: - """AnyIO Backend.""" - return "asyncio" diff --git a/.conflict-side-0/tests/logging/__init__.py b/.conflict-side-0/tests/logging/__init__.py deleted file mode 100644 index a1c677a..0000000 --- a/.conflict-side-0/tests/logging/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Logging Tests.""" diff --git a/.conflict-side-0/tests/logging/test_loguru.py b/.conflict-side-0/tests/logging/test_loguru.py deleted file mode 100644 index 9214250..0000000 --- a/.conflict-side-0/tests/logging/test_loguru.py +++ /dev/null @@ -1,274 +0,0 @@ -"""Test Logging Loguru.""" - -from collections.abc import Generator -from datetime import datetime -from io import StringIO - -import pytest -from loguru import logger -from pydantic import TypeAdapter - -from grelmicro.errors import DependencyNotFoundError -from grelmicro.logging.errors import LoggingSettingsValidationError -from grelmicro.logging.loguru import ( - JSON_FORMAT, - JSONRecordDict, - configure_logging, - json_formatter, - json_patcher, -) - -json_record_type_adapter = TypeAdapter(JSONRecordDict) - - -@pytest.fixture(autouse=True) -def cleanup_handlers() -> Generator[None, None, None]: - """Cleanup logging handlers.""" - logger.configure(handlers=[]) - yield - logger.remove() - - -def generate_logs() -> int: - """Generate logs.""" - logger.debug("Hello, World!") - logger.info("Hello, World!") - logger.warning("Hello, World!") - logger.error("Hello, Alice!", user="Alice") - try: - 1 / 0 # noqa: B018 - except ZeroDivisionError: - logger.exception("Hello, Bob!") - - return 5 - - -def assert_logs(logs: str) -> None: - """Assert logs.""" - ( - info, - warning, - error, - exception, - ) = ( - json_record_type_adapter.validate_json(line) - for line in logs.splitlines()[0:4] - ) - - expected_separator = 3 - - assert info["logger"] - assert info["logger"].startswith("tests.logging.test_loguru:generate_logs:") - assert len(info["logger"].split(":")) == expected_separator - assert info["time"] == datetime.fromisoformat(info["time"]).isoformat() - assert info["level"] == "INFO" - assert info["msg"] == "Hello, World!" - assert info["thread"] == "MainThread" - assert "ctx" not in info - - assert warning["logger"] - assert warning["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(warning["logger"].split(":")) == expected_separator - assert ( - warning["time"] == datetime.fromisoformat(warning["time"]).isoformat() - ) - assert warning["level"] == "WARNING" - assert warning["msg"] == "Hello, World!" - assert warning["thread"] == "MainThread" - assert "ctx" not in warning - - assert error["logger"] - assert error["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(error["logger"].split(":")) == expected_separator - assert error["time"] == datetime.fromisoformat(error["time"]).isoformat() - assert error["level"] == "ERROR" - assert error["msg"] == "Hello, Alice!" - assert error["thread"] == "MainThread" - assert error["ctx"] == {"user": "Alice"} - - assert exception["logger"] - assert exception["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(exception["logger"].split(":")) == expected_separator - assert ( - exception["time"] - == datetime.fromisoformat(exception["time"]).isoformat() - ) - assert exception["level"] == "ERROR" - assert exception["msg"] == "Hello, Bob!" - assert exception["thread"] == "MainThread" - assert exception["ctx"] == { - "exception": "ZeroDivisionError: division by zero", - } - - -def test_json_formatter() -> None: - """Test JSON Formatter.""" - # Arrange - sink = StringIO() - - # Act - logger.add(sink, format=json_formatter, level="INFO") - generate_logs() - - # Assert - assert_logs(sink.getvalue()) - - -def test_json_patching() -> None: - """Test JSON Patching.""" - # Arrange - sink = StringIO() - - # Act - # logger.patch(json_patcher) -> Patch is not working using logger.configure instead - logger.configure(patcher=json_patcher) - logger.add(sink, format=JSON_FORMAT, level="INFO") - generate_logs() - - # Assert - assert_logs(sink.getvalue()) - - -def test_configure_logging_default( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Default.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - configure_logging() - generate_logs() - - # Assert - assert_logs(capsys.readouterr().out) - - -def test_configure_logging_text( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Text.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "text") - - # Act - configure_logging() - generate_logs() - - # Assert - lines = capsys.readouterr().out.splitlines() - - assert "tests.logging.test_loguru:generate_logs:" in lines[0] - assert " | INFO | " in lines[0] - assert " - Hello, World!" in lines[0] - - assert "tests.logging.test_loguru:generate_logs:" in lines[1] - assert " | WARNING | " in lines[1] - assert " - Hello, World!" in lines[1] - - assert "tests.logging.test_loguru:generate_logs:" in lines[2] - assert " | ERROR | " in lines[2] - assert " - Hello, Alice!" in lines[2] - - assert "tests.logging.test_loguru:generate_logs:" in lines[3] - assert " | ERROR | " in lines[3] - assert " - Hello, Bob!" in lines[3] - assert "Traceback" in lines[4] - assert "ZeroDivisionError: division by zero" in lines[-1] - - -def test_configure_logging_json( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging JSON.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "json") - - # Act - configure_logging() - generate_logs() - - # Assert - assert_logs(capsys.readouterr().out) - - -def test_configure_logging_level( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Level.""" - # Arrange - monkeypatch.setenv("LOG_LEVEL", "DEBUG") - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - configure_logging() - logs_count = generate_logs() - - # Assert - assert len(capsys.readouterr().out.splitlines()) == logs_count - - -def test_configure_logging_invalid_level( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Invalid Level.""" - # Arrange - monkeypatch.setenv("LOG_LEVEL", "INVALID") - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - with pytest.raises( - LoggingSettingsValidationError, - match=( - r"Could not validate environment variables settings:\n" - r"- LOG_LEVEL: Input should be 'DEBUG', 'INFO', 'WARNING', 'ERROR' or 'CRITICAL'" - r" \[input=INVALID\]" - ), - ): - configure_logging() - - # Assert - assert not capsys.readouterr().out - - -def test_configure_logging_format_template( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Format Template.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "{level}: {message}") - - # Act - configure_logging() - generate_logs() - - # Assert - lines = capsys.readouterr().out.splitlines() - assert "INFO: Hello, World!" in lines[0] - assert "WARNING: Hello, World!" in lines[1] - assert "ERROR: Hello, Alice!" in lines[2] - assert "ERROR: Hello, Bob!" in lines[3] - assert "Traceback" in lines[4] - assert "ZeroDivisionError: division by zero" in lines[-1] - - -def test_configure_logging_dependency_not_found( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test Configure Logging Dependency Not Found.""" - # Arrange - monkeypatch.setattr("grelmicro.logging.loguru.loguru", None) - - # Act / Assert - with pytest.raises(DependencyNotFoundError, match="loguru"): - configure_logging() diff --git a/.conflict-side-0/tests/sync/__init__.py b/.conflict-side-0/tests/sync/__init__.py deleted file mode 100644 index 5e3b5c4..0000000 --- a/.conflict-side-0/tests/sync/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Synchronization Primitives Tests.""" diff --git a/.conflict-side-0/tests/sync/test_backends.py b/.conflict-side-0/tests/sync/test_backends.py deleted file mode 100644 index b08a92f..0000000 --- a/.conflict-side-0/tests/sync/test_backends.py +++ /dev/null @@ -1,370 +0,0 @@ -"""Test Synchronization Backends.""" - -from collections.abc import AsyncGenerator, Callable, Generator -from uuid import uuid4 - -import pytest -from anyio import sleep -from testcontainers.core.container import DockerContainer -from testcontainers.postgres import PostgresContainer -from testcontainers.redis import RedisContainer - -from grelmicro.sync._backends import get_sync_backend, loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import BackendNotLoadedError -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.sync.postgres import PostgresSyncBackend -from grelmicro.sync.redis import RedisSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(15)] - - -@pytest.fixture(scope="module") -def anyio_backend() -> str: - """AnyIO Backend Module Scope.""" - return "asyncio" - - -@pytest.fixture(scope="module") -def monkeypatch() -> Generator[pytest.MonkeyPatch, None, None]: - """Monkeypatch Module Scope.""" - monkeypatch = pytest.MonkeyPatch() - yield monkeypatch - monkeypatch.undo() - - -@pytest.fixture -def clean_registry() -> Generator[None, None, None]: - """Make sure the registry is clean.""" - loaded_backends.pop("lock", None) - yield - loaded_backends.pop("lock", None) - - -@pytest.fixture( - params=[ - "memory", - pytest.param("redis", marks=[pytest.mark.integration]), - pytest.param("postgres", marks=[pytest.mark.integration]), - ], - scope="module", -) -def backend_name(request: pytest.FixtureRequest) -> str: - """Backend Name.""" - return request.param - - -@pytest.fixture( - scope="module", -) -def container( - backend_name: str, - monkeypatch: pytest.MonkeyPatch, -) -> Generator[DockerContainer | None, None, None]: - """Test Container for each Backend.""" - if backend_name == "redis": - with RedisContainer() as container: - yield container - elif backend_name == "postgres": - monkeypatch.setenv("POSTGRES_HOST", "localhost") - monkeypatch.setenv("POSTGRES_PORT", "5432") - monkeypatch.setenv("POSTGRES_DB", "test") - monkeypatch.setenv("POSTGRES_USER", "test") - monkeypatch.setenv("POSTGRES_PASSWORD", "test") - with PostgresContainer() as container: - yield container - elif backend_name == "memory": - yield None - - -@pytest.fixture(scope="module") -async def backend( - backend_name: str, container: DockerContainer | None -) -> AsyncGenerator[SyncBackend]: - """Test Container for each Backend.""" - if backend_name == "redis" and container: - port = container.get_exposed_port(6379) - async with RedisSyncBackend(f"redis://localhost:{port}/0") as backend: - yield backend - elif backend_name == "postgres" and container: - port = container.get_exposed_port(5432) - async with PostgresSyncBackend( - f"postgresql://test:test@localhost:{port}/test" - ) as backend: - yield backend - elif backend_name == "memory": - async with MemorySyncBackend() as backend: - yield backend - - -async def test_acquire(backend: SyncBackend) -> None: - """Test acquire.""" - # Arrange - name = "test_acquire" - token = uuid4().hex - duration = 1 - - # Act - result = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result - - -async def test_acquire_reantrant(backend: SyncBackend) -> None: - """Test acquire is reantrant.""" - # Arrange - name = "test_acquire_reantrant" - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result1 - assert result2 - - -async def test_acquire_already_acquired(backend: SyncBackend) -> None: - """Test acquire when already acquired.""" - # Arrange - name = "test_acquire_already_acquired" - token1 = uuid4().hex - token2 = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token1, duration=duration) - result2 = await backend.acquire(name=name, token=token2, duration=duration) - - # Assert - assert token1 != token2 - assert result1 - assert not result2 - - -async def test_acquire_expired(backend: SyncBackend) -> None: - """Test acquire when expired.""" - # Arrange - name = "test_acquire_expired" - token = uuid4().hex - duration = 0.01 - - # Act - result = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result - assert result2 - - -async def test_acquire_already_acquired_expired(backend: SyncBackend) -> None: - """Test acquire when already acquired but expired.""" - # Arrange - name = "test_acquire_already_acquired_expired" + uuid4().hex - token1 = uuid4().hex - token2 = uuid4().hex - duration = 0.01 - - # Act - result = await backend.acquire(name=name, token=token1, duration=duration) - await sleep(duration * 2) - result2 = await backend.acquire(name=name, token=token2, duration=duration) - - # Assert - assert token1 != token2 - assert result - assert result2 - - -async def test_release_not_acquired(backend: SyncBackend) -> None: - """Test release when not acquired.""" - # Arrange - name = "test_release" + uuid4().hex - token = uuid4().hex - - # Act - result = await backend.release(name=name, token=token) - - # Assert - assert not result - - -async def test_release_acquired(backend: SyncBackend) -> None: - """Test release when acquired.""" - # Arrange - name = "test_release_acquired" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert result2 - - -async def test_release_not_reantrant(backend: SyncBackend) -> None: - """Test release is not reantrant.""" - # Arrange - name = "test_release_not_reantrant" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.release(name=name, token=token) - result3 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert result2 - assert not result3 - - -async def test_release_acquired_expired(backend: SyncBackend) -> None: - """Test release when acquired but expired.""" - # Arrange - name = "test_release_acquired_expired" + uuid4().hex - token = uuid4().hex - duration = 0.01 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert not result2 - - -async def test_release_not_acquired_expired(backend: SyncBackend) -> None: - """Test release when not acquired but expired.""" - # Arrange - name = "test_release_not_acquired_expired" + uuid4().hex - token = uuid4().hex - duration = 0.01 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert not result2 - - -async def test_locked(backend: SyncBackend) -> None: - """Test locked.""" - # Arrange - name = "test_locked" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - locked_before = await backend.locked(name=name) - await backend.acquire(name=name, token=token, duration=duration) - locked_after = await backend.locked(name=name) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_owned(backend: SyncBackend) -> None: - """Test owned.""" - # Arrange - name = "test_owned" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - owned_before = await backend.owned(name=name, token=token) - await backend.acquire(name=name, token=token, duration=duration) - owned_after = await backend.owned(name=name, token=token) - - # Assert - assert owned_before is False - assert owned_after is True - - -async def test_owned_another(backend: SyncBackend) -> None: - """Test owned another.""" - # Arrange - name = "test_owned_another" + uuid4().hex - token1 = uuid4().hex - token2 = uuid4().hex - duration = 1 - - # Act - owned_before = await backend.owned(name=name, token=token1) - await backend.acquire(name=name, token=token1, duration=duration) - owned_after = await backend.owned(name=name, token=token2) - - # Assert - assert owned_before is False - assert owned_after is False - - -@pytest.mark.parametrize( - "backend_factory", - [ - lambda: MemorySyncBackend(), - lambda: RedisSyncBackend("redis://localhost:6379/0"), - lambda: PostgresSyncBackend( - "postgresql://user:password@localhost:5432/db" - ), - ], -) -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend(backend_factory: Callable[[], SyncBackend]) -> None: - """Test Get Synchronization Backend.""" - # Arrange - expected_backend = backend_factory() - - # Act - backend = get_sync_backend() - - # Assert - assert backend is expected_backend - - -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend_not_loaded() -> None: - """Test Get Synchronization Backend Not Loaded.""" - # Act / Assert - with pytest.raises(BackendNotLoadedError): - get_sync_backend() - - -@pytest.mark.parametrize( - "backend_factory", - [ - lambda: MemorySyncBackend(auto_register=False), - lambda: RedisSyncBackend( - "redis://localhost:6379/0", auto_register=False - ), - lambda: PostgresSyncBackend( - "postgresql://user:password@localhost:5432/db", auto_register=False - ), - ], -) -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend_auto_register_disabled( - backend_factory: Callable[[], SyncBackend], -) -> None: - """Test Get Synchronization Backend.""" - # Arrange - backend_factory() - - # Act / Assert - with pytest.raises(BackendNotLoadedError): - get_sync_backend() diff --git a/.conflict-side-0/tests/sync/test_leaderelection.py b/.conflict-side-0/tests/sync/test_leaderelection.py deleted file mode 100644 index d357daa..0000000 --- a/.conflict-side-0/tests/sync/test_leaderelection.py +++ /dev/null @@ -1,457 +0,0 @@ -"""Test leader election.""" - -import math - -import pytest -from anyio import Event, create_task_group, sleep -from pydantic import ValidationError -from pytest_mock import MockerFixture - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.leaderelection import LeaderElection, LeaderElectionConfig -from grelmicro.sync.memory import MemorySyncBackend - -WORKERS = 4 -WORKER_1 = 0 -WORKER_2 = 1 -TEST_TIMEOUT = 1 - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(TEST_TIMEOUT)] - - -@pytest.fixture -def backend() -> SyncBackend: - """Return Memory Synchronization Backend.""" - return MemorySyncBackend() - - -@pytest.fixture -def configs() -> list[LeaderElectionConfig]: - """Leader election Config.""" - return [ - LeaderElectionConfig( - name="test_leader_election", - worker=f"worker_{i}", - lease_duration=0.02, - renew_deadline=0.015, - retry_interval=0.005, - error_interval=0.01, - backend_timeout=0.005, - ) - for i in range(WORKERS) - ] - - -@pytest.fixture -def leader_elections( - backend: SyncBackend, configs: list[LeaderElectionConfig] -) -> list[LeaderElection]: - """Leader elections.""" - return [ - LeaderElection(backend=backend, **configs[i].model_dump()) - for i in range(WORKERS) - ] - - -@pytest.fixture -def leader_election( - backend: SyncBackend, configs: list[LeaderElectionConfig] -) -> LeaderElection: - """Leader election.""" - return LeaderElection(backend=backend, **configs[WORKER_1].model_dump()) - - -async def wait_first_leader(leader_elections: list[LeaderElection]) -> None: - """Wait for the first leader to be elected.""" - - async def wrapper(leader_election: LeaderElection, event: Event) -> None: - """Wait for the leadership.""" - await leader_election.wait_for_leader() - event.set() - - async with create_task_group() as task_group: - event = Event() - for coroutine in leader_elections: - task_group.start_soon(wrapper, coroutine, event) - await event.wait() - task_group.cancel_scope.cancel() - - -def test_leader_election_config() -> None: - """Test leader election Config.""" - # Arrange - config = LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - lease_duration=0.01, - renew_deadline=0.008, - retry_interval=0.001, - error_interval=0.01, - backend_timeout=0.007, - ) - - # Assert - assert config.model_dump() == { - "name": "test_leader_election", - "worker": "worker_1", - "lease_duration": 0.01, - "renew_deadline": 0.008, - "retry_interval": 0.001, - "error_interval": 0.01, - "backend_timeout": 0.007, - } - - -def test_leader_election_config_defaults() -> None: - """Test leader election Config Defaults.""" - # Arrange - config = LeaderElectionConfig( - name="test_leader_election", worker="worker_1" - ) - - # Assert - assert config.model_dump() == { - "name": "test_leader_election", - "worker": "worker_1", - "lease_duration": 15, - "renew_deadline": 10, - "retry_interval": 2, - "error_interval": 30, - "backend_timeout": 5, - } - - -def test_leader_election_config_validation_errors() -> None: - """Test leader election Config Errors.""" - # Arrange - with pytest.raises( - ValidationError, - match="Renew deadline must be shorter than lease duration", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - lease_duration=15, - renew_deadline=20, - ) - with pytest.raises( - ValidationError, - match="Retry interval must be shorter than renew deadline", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - renew_deadline=10, - retry_interval=15, - ) - with pytest.raises( - ValidationError, - match="Backend timeout must be shorter than renew deadline", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - renew_deadline=10, - backend_timeout=15, - ) - - -async def test_lifecycle(leader_election: LeaderElection) -> None: - """Test leader election on worker complete lifecycle.""" - # Act - is_leader_before_start = leader_election.is_leader() - is_running_before_start = leader_election.is_running() - async with create_task_group() as tg: - await tg.start(leader_election) - is_running_after_start = leader_election.is_running() - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - tg.cancel_scope.cancel() - is_running_after_cancel = leader_election.is_running() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_cancel is False - - assert is_running_before_start is False - assert is_running_after_start is True - assert is_running_after_cancel is False - - -async def test_leader_election_context_manager( - leader_election: LeaderElection, -) -> None: - """Test leader election on worker using context manager.""" - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - async with leader_election: - is_leader_inside_context = leader_election.is_leader() - is_leader_after_context = leader_election.is_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_inside_context is True - assert is_leader_after_context is True - assert is_leader_after_cancel is False - - -async def test_leader_election_single_worker( - leader_election: LeaderElection, -) -> None: - """Test leader election on single worker.""" - # Act - async with create_task_group() as tg: - is_leader_before_start = leader_election.is_leader() - await tg.start(leader_election) - is_leader_inside_context = leader_election.is_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_inside_context is True - assert is_leader_after_cancel is False - - -async def test_leadership_abandon_on_renew_deadline_reached( - leader_election: LeaderElection, -) -> None: - """Test leader election abandons leadership when renew deadline is reached.""" - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - leader_election.config.retry_interval = math.inf - await leader_election.wait_lose_leader() - is_leader_after_not_renewed = leader_election.is_leader() - tg.cancel_scope.cancel() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_not_renewed is False - - -async def test_leadership_abandon_on_backend_failure( - leader_election: LeaderElection, - caplog: pytest.LogCaptureFixture, - mocker: MockerFixture, -) -> None: - """Test leader election abandons leadership when backend is unreachable.""" - # Arrange - caplog.set_level("WARNING") - - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - mocker.patch.object( - leader_election.backend, - "acquire", - side_effect=Exception("Backend Unreachable"), - ) - await leader_election.wait_lose_leader() - is_leader_after_not_renewed = leader_election.is_leader() - tg.cancel_scope.cancel() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_not_renewed is False - assert ( - "Leader Election lost leadership: test_leader_election (renew deadline reached)" - in caplog.messages - ) - - -async def test_unepexpected_stop( - leader_election: LeaderElection, mocker: MockerFixture -) -> None: - """Test leader election worker abandons leadership on unexpected stop.""" - - # Arrange - async def leader_election_unexpected_exception() -> None: - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - mock = mocker.patch.object( - leader_election, - "_try_acquire_or_renew", - side_effect=Exception("Unexpected Exception"), - ) - await leader_election.wait_lose_leader() - mock.reset_mock() - tg.cancel_scope.cancel() - - # Act / Assert - with pytest.raises(ExceptionGroup): - await leader_election_unexpected_exception() - - -async def test_release_on_cancel( - backend: SyncBackend, leader_election: LeaderElection, mocker: MockerFixture -) -> None: - """Test leader election on worker that releases the lock on cancel.""" - # Arrange - spy_release = mocker.spy(backend, "release") - - # Act - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - - # Assert - spy_release.assert_called_once() - - -async def test_release_failure_ignored( - backend: SyncBackend, - leader_election: LeaderElection, - mocker: MockerFixture, -) -> None: - """Test leader election on worker that ignores release failure.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Unreachable") - ) - - # Act - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - - -async def test_only_one_leader(leader_elections: list[LeaderElection]) -> None: - """Test leader election on multiple workers ensuring only one leader is elected.""" - # Act - leaders_before_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - async with create_task_group() as tg: - for leader_election in leader_elections: - await tg.start(leader_election) - await wait_first_leader(leader_elections) - leaders_after_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - tg.cancel_scope.cancel() - for leader_election in leader_elections: - await leader_election.wait_lose_leader() - leaders_after_cancel = [ - leader_election.is_leader() for leader_election in leader_elections - ] - - # Assert - assert sum(leaders_before_start) == 0 - assert sum(leaders_after_start) == 1 - assert sum(leaders_after_cancel) == 0 - - -async def test_leader_transition( - leader_elections: list[LeaderElection], -) -> None: - """Test leader election leader transition to another worker.""" - # Arrange - leaders_after_leader_election1_start = [False] * len(leader_elections) - leaders_after_all_start = [False] * len(leader_elections) - leaders_after_leader_election1_down = [False] * len(leader_elections) - - # Act - leaders_before_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - async with create_task_group() as workers_tg: - async with create_task_group() as worker1_tg: - await worker1_tg.start(leader_elections[WORKER_1]) - await leader_elections[WORKER_1].wait_for_leader() - leaders_after_leader_election1_start = [ - leader_election.is_leader() - for leader_election in leader_elections - ] - - for leader_election in leader_elections: - await workers_tg.start(leader_election) - leaders_after_all_start = [ - leader_election.is_leader() - for leader_election in leader_elections - ] - worker1_tg.cancel_scope.cancel() - - await leader_elections[WORKER_1].wait_lose_leader() - - await wait_first_leader(leader_elections) - leaders_after_leader_election1_down = [ - leader_election.is_leader() for leader_election in leader_elections - ] - workers_tg.cancel_scope.cancel() - - for leader_election in leader_elections[WORKER_2:]: - await leader_election.wait_lose_leader() - leaders_after_all_down = [ - leader_election.is_leader() for leader_election in leader_elections - ] - - # Assert - assert sum(leaders_before_start) == 0 - assert sum(leaders_after_leader_election1_start) == 1 - assert sum(leaders_after_all_start) == 1 - assert sum(leaders_after_leader_election1_down) == 1 - assert sum(leaders_after_all_down) == 0 - - assert leaders_after_leader_election1_start[WORKER_1] is True - assert leaders_after_leader_election1_down[WORKER_1] is False - - -async def test_error_interval( - backend: SyncBackend, - leader_elections: list[LeaderElection], - caplog: pytest.LogCaptureFixture, - mocker: MockerFixture, -) -> None: - """Test leader election on worker with error cooldown.""" - # Arrange - caplog.set_level("ERROR") - leader_elections[WORKER_1].config.error_interval = 1 - leader_elections[WORKER_2].config.error_interval = 0.001 - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Unreachable") - ) - - # Act - async with create_task_group() as tg: - await tg.start(leader_elections[WORKER_1]) - await sleep(0.01) - tg.cancel_scope.cancel() - leader_election1_nb_errors = sum( - 1 for record in caplog.records if record.levelname == "ERROR" - ) - caplog.clear() - - async with create_task_group() as tg: - await tg.start(leader_elections[WORKER_2]) - await sleep(0.01) - tg.cancel_scope.cancel() - leader_election2_nb_errors = sum( - 1 for record in caplog.records if record.levelname == "ERROR" - ) - - # Assert - assert leader_election1_nb_errors == 1 - assert leader_election2_nb_errors >= 1 diff --git a/.conflict-side-0/tests/sync/test_lock.py b/.conflict-side-0/tests/sync/test_lock.py deleted file mode 100644 index 42e0b04..0000000 --- a/.conflict-side-0/tests/sync/test_lock.py +++ /dev/null @@ -1,506 +0,0 @@ -"""Test Lock.""" - -import time -from collections.abc import AsyncGenerator - -import pytest -from anyio import WouldBlock, sleep, to_thread -from pytest_mock import MockerFixture - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import ( - LockAcquireError, - LockNotOwnedError, - LockReleaseError, - SyncBackendError, -) -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -WORKER_1 = 0 -WORKER_2 = 1 -WORKER_COUNT = 2 - -LOCK_NAME = "test_leased_lock" - - -@pytest.fixture -async def backend() -> AsyncGenerator[SyncBackend]: - """Return Memory Synchronization Backend.""" - async with MemorySyncBackend() as backend: - yield backend - - -@pytest.fixture -def locks(backend: SyncBackend) -> list[Lock]: - """Locks of multiple workers.""" - return [ - Lock( - backend=backend, - name=LOCK_NAME, - worker=f"worker_{i}", - lease_duration=0.01, - retry_interval=0.001, - ) - for i in range(WORKER_COUNT) - ] - - -@pytest.fixture -def lock(locks: list[Lock]) -> Lock: - """Lock.""" - return locks[WORKER_1] - - -async def test_lock_owned(locks: list[Lock]) -> None: - """Test Lock owned.""" - # Act - worker_1_owned_before = await locks[WORKER_1].owned() - worker_2_owned_before = await locks[WORKER_2].owned() - await locks[WORKER_1].acquire() - worker_1_owned_after = await locks[WORKER_1].owned() - worker_2_owned_after = await locks[WORKER_2].owned() - - # Assert - assert worker_1_owned_before is False - assert worker_2_owned_before is False - assert worker_1_owned_after is True - assert worker_2_owned_after is False - - -async def test_lock_from_thread_owned(locks: list[Lock]) -> None: - """Test Lock from thread owned.""" - # Arrange - worker_1_owned_before = None - worker_2_owned_before = None - worker_1_owned_after = None - worker_2_owned_after = None - - # Act - def sync() -> None: - nonlocal worker_1_owned_before - nonlocal worker_2_owned_before - nonlocal worker_1_owned_after - nonlocal worker_2_owned_after - - worker_1_owned_before = locks[WORKER_1].from_thread.owned() - worker_2_owned_before = locks[WORKER_2].from_thread.owned() - locks[WORKER_1].from_thread.acquire() - worker_1_owned_after = locks[WORKER_1].from_thread.owned() - worker_2_owned_after = locks[WORKER_2].from_thread.owned() - - await to_thread.run_sync(sync) - - # Assert - assert worker_1_owned_before is False - assert worker_2_owned_before is False - assert worker_1_owned_after is True - assert worker_2_owned_after is False - - -async def test_lock_context_manager(lock: Lock) -> None: - """Test Lock context manager.""" - # Act - locked_before = await lock.locked() - async with lock: - locked_inside = await lock.locked() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_inside is True - assert locked_after is False - - -async def test_lock_from_thread_context_manager_acquire(lock: Lock) -> None: - """Test Lock from thread context manager.""" - # Arrange - locked_before = None - locked_inside = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_inside - nonlocal locked_after - - locked_before = lock.from_thread.locked() - with lock.from_thread: - locked_inside = lock.from_thread.locked() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_inside is True - assert locked_after is False - - -async def test_lock_context_manager_wait(lock: Lock, locks: list[Lock]) -> None: - """Test Lock context manager wait.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act - locked_before = await lock.locked() - async with locks[WORKER_2]: # Wait until lock expires - locked_inside = await lock.locked() - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_inside is True - assert locked_after is False - - -async def test_lock_from_thread_context_manager_wait( - lock: Lock, locks: list[Lock] -) -> None: - """Test Lock from thread context manager wait.""" - # Arrange - locked_before = None - locked_inside = None - locked_after = None - await locks[WORKER_1].acquire() - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_inside - nonlocal locked_after - - locked_before = lock.from_thread.locked() - with locks[WORKER_2].from_thread: - locked_inside = lock.from_thread.locked() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is True - assert locked_inside is True - assert locked_after is False - - -async def test_lock_acquire(lock: Lock) -> None: - """Test Lock acquire.""" - # Act - locked_before = await lock.locked() - await lock.acquire() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_from_thread_acquire(lock: Lock) -> None: - """Test Lock from thread acquire.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_wait(lock: Lock, locks: list[Lock]) -> None: - """Test Lock acquire wait.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act - locked_before = await lock.locked() - await locks[WORKER_2].acquire() # Wait until lock expires - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_after is True - - -async def test_lock_from_thread_acquire_wait(lock: Lock) -> None: - """Test Lock from thread acquire wait.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_nowait(lock: Lock) -> None: - """Test Lock wait acquire.""" - # Act - locked_before = await lock.locked() - await lock.acquire_nowait() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_from_thread_acquire_nowait(lock: Lock) -> None: - """Test Lock from thread wait acquire.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire_nowait() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_nowait_would_block(locks: list[Lock]) -> None: - """Test Lock wait acquire would block.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act / Assert - with pytest.raises(WouldBlock): - await locks[WORKER_2].acquire_nowait() - - -async def test_lock_from_thread_acquire_nowait_would_block( - locks: list[Lock], -) -> None: - """Test Lock from thread wait acquire would block.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act / Assert - def sync() -> None: - with pytest.raises(WouldBlock): - locks[WORKER_2].from_thread.acquire_nowait() - - await to_thread.run_sync(sync) - - -async def test_lock_release(lock: Lock) -> None: - """Test Lock release.""" - # Act / Assert - with pytest.raises(LockNotOwnedError): - await lock.release() - - -async def test_lock_from_thread_release(lock: Lock) -> None: - """Test Lock from thread release.""" - - # Act / Assert - def sync() -> None: - with pytest.raises(LockNotOwnedError): - lock.from_thread.release() - - await to_thread.run_sync(sync) - - -async def test_lock_release_acquired(lock: Lock) -> None: - """Test Lock release acquired.""" - # Arrange - await lock.acquire() - - # Act - locked_before = await lock.locked() - await lock.release() - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_after is False - - -async def test_lock_from_thread_release_acquired(lock: Lock) -> None: - """Test Lock from thread release acquired.""" - # Arrange - locked_before = None - locked_after = None - - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - lock.from_thread.acquire() - - # Act - locked_before = lock.from_thread.locked() - lock.from_thread.release() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is True - assert locked_after is False - - -async def test_lock_release_expired(locks: list[Lock]) -> None: - """Test Lock release expired.""" - # Arrange - await locks[WORKER_1].acquire() - await sleep(locks[WORKER_1].config.lease_duration) - - # Act - worker_1_locked_before = await locks[WORKER_1].locked() - with pytest.raises(LockNotOwnedError): - await locks[WORKER_2].release() - - # Assert - assert worker_1_locked_before is False - - -async def test_lock_from_thread_release_expired(locks: list[Lock]) -> None: - """Test Lock from thread release expired.""" - # Arrange - worker_1_locked_before = None - - def sync() -> None: - nonlocal worker_1_locked_before - - locks[WORKER_1].from_thread.acquire() - time.sleep(locks[WORKER_1].config.lease_duration) - - # Act - worker_1_locked_before = locks[WORKER_1].from_thread.locked() - with pytest.raises(LockNotOwnedError): - locks[WORKER_2].from_thread.release() - - await to_thread.run_sync(sync) - - # Assert - assert worker_1_locked_before is False - - -async def test_lock_acquire_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock acquire backend error.""" - # Arrange - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Error") - ) - - # Act - with pytest.raises(LockAcquireError): - await lock.acquire() - - -async def test_lock_from_thread_acquire_backend_error( - backend: SyncBackend, - lock: Lock, - mocker: MockerFixture, -) -> None: - """Test Lock from thread acquire backend error.""" - # Arrange - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Error") - ) - - # Act - def sync() -> None: - with pytest.raises(LockAcquireError): - lock.from_thread.acquire() - - await to_thread.run_sync(sync) - - -async def test_lock_release_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock release backend error.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Error") - ) - - # Act - await lock.acquire() - with pytest.raises(LockReleaseError): - await lock.release() - - -async def test_lock_from_thread_release_backend_error( - backend: SyncBackend, - lock: Lock, - mocker: MockerFixture, -) -> None: - """Test Lock from thread release backend error.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Error") - ) - - # Act - def sync() -> None: - lock.from_thread.acquire() - with pytest.raises(LockReleaseError): - lock.from_thread.release() - - await to_thread.run_sync(sync) - - -async def test_lock_owned_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock owned backend error.""" - # Arrange - mocker.patch.object( - backend, "owned", side_effect=Exception("Backend Error") - ) - - # Act / Assert - with pytest.raises(SyncBackendError): - await lock.owned() - - -async def test_lock_locked_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock locked backend error.""" - # Arrange - mocker.patch.object( - backend, "locked", side_effect=Exception("Backend Error") - ) - - # Act / Assert - with pytest.raises(SyncBackendError): - await lock.locked() diff --git a/.conflict-side-0/tests/sync/test_postgres.py b/.conflict-side-0/tests/sync/test_postgres.py deleted file mode 100644 index ef8dd18..0000000 --- a/.conflict-side-0/tests/sync/test_postgres.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Tests for PostgreSQL Backends.""" - -import pytest - -from grelmicro.errors import OutOfContextError -from grelmicro.sync.errors import SyncSettingsValidationError -from grelmicro.sync.postgres import PostgresSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -URL = "postgres://user:password@localhost:5432/db" - - -@pytest.mark.parametrize( - "table_name", - [ - "locks table", - "%locks", - "locks;table", - "locks' OR '1'='1", - "locks; DROP TABLE users; --", - ], -) -def test_sync_backend_table_name_invalid(table_name: str) -> None: - """Test Synchronization Backend Table Name Invalid.""" - # Act / Assert - with pytest.raises( - ValueError, match="Table name '.*' is not a valid identifier" - ): - PostgresSyncBackend(url=URL, table_name=table_name) - - -async def test_sync_backend_out_of_context_errors() -> None: - """Test Synchronization Backend Out Of Context Errors.""" - # Arrange - backend = PostgresSyncBackend(url=URL) - name = "lock" - key = "token" - - # Act / Assert - with pytest.raises(OutOfContextError): - await backend.acquire(name=name, token=key, duration=1) - with pytest.raises(OutOfContextError): - await backend.release(name=name, token=key) - with pytest.raises(OutOfContextError): - await backend.locked(name=name) - with pytest.raises(OutOfContextError): - await backend.owned(name=name, token=key) - - -@pytest.mark.parametrize( - ("environs"), - [ - { - "POSTGRES_URL": "postgresql://test_user:test_password@test_host:1234/test_db" - }, - { - "POSTGRES_USER": "test_user", - "POSTGRES_PASSWORD": "test_password", - "POSTGRES_HOST": "test_host", - "POSTGRES_PORT": "1234", - "POSTGRES_DB": "test_db", - }, - ], -) -def test_postgres_env_var_settings( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test PostgreSQL Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Act - backend = PostgresSyncBackend() - - # Assert - assert ( - backend._url - == "postgresql://test_user:test_password@test_host:1234/test_db" - ) - - -@pytest.mark.parametrize( - ("environs"), - [ - { - "POSTGRES_URL": "test://test_user:test_password@test_host:1234/test_db" - }, - {"POSTGRES_USER": "test_user"}, - ], -) -def test_postgres_env_var_settings_validation_error( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test PostgreSQL Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Assert / Act - with pytest.raises( - SyncSettingsValidationError, - match=(r"Could not validate environment variables settings:\n"), - ): - PostgresSyncBackend() diff --git a/.conflict-side-0/tests/sync/test_redis.py b/.conflict-side-0/tests/sync/test_redis.py deleted file mode 100644 index a14bad7..0000000 --- a/.conflict-side-0/tests/sync/test_redis.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Tests for Redis Backends.""" - -import pytest - -from grelmicro.sync.errors import SyncSettingsValidationError -from grelmicro.sync.redis import RedisSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -URL = "redis://:test_password@test_host:1234/0" - - -@pytest.mark.parametrize( - ("environs"), - [ - {"REDIS_URL": URL}, - { - "REDIS_PASSWORD": "test_password", - "REDIS_HOST": "test_host", - "REDIS_PORT": "1234", - "REDIS_DB": "0", - }, - ], -) -def test_redis_env_var_settings( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Redis Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Act - backend = RedisSyncBackend() - - # Assert - assert backend._url == URL - - -@pytest.mark.parametrize( - ("environs"), - [ - {"REDIS_URL": "test://:test_password@test_host:1234/0"}, - {"REDIS_PASSWORD": "test_password"}, - { - "REDIS_URL": "test://:test_password@test_host:1234/0", - "REDIS_PASSWORD": "test_password", - "REDIS_HOST": "test_host", - "REDIS_PORT": "1234", - "REDIS_DB": "0", - }, - ], -) -def test_redis_env_var_settings_validation_error( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Redis Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Assert / Act - with pytest.raises( - SyncSettingsValidationError, - match=(r"Could not validate environment variables settings:\n"), - ): - RedisSyncBackend() diff --git a/.conflict-side-0/tests/sync/utils.py b/.conflict-side-0/tests/sync/utils.py deleted file mode 100644 index e20356b..0000000 --- a/.conflict-side-0/tests/sync/utils.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Test utilities for Lock.""" - -from anyio import Event, create_task_group, fail_after - -from grelmicro.sync._base import BaseLock - - -async def wait_first_acquired(locks: list[BaseLock]) -> None: - """Wait for the first lock to be acquired.""" - - async def wrapper(lock: BaseLock, event: Event) -> None: - """Send event when lock is acquired.""" - with fail_after(1): - await lock.acquire() - event.set() - - with fail_after(1): - async with create_task_group() as task_group: - event = Event() - for lock in locks: - task_group.start_soon(wrapper, lock, event) - await event.wait() - task_group.cancel_scope.cancel() diff --git a/.conflict-side-0/tests/task/__init__.py b/.conflict-side-0/tests/task/__init__.py deleted file mode 100644 index ebf85b3..0000000 --- a/.conflict-side-0/tests/task/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Task Scheduler Tests.""" diff --git a/.conflict-side-0/tests/task/samples.py b/.conflict-side-0/tests/task/samples.py deleted file mode 100644 index d19c153..0000000 --- a/.conflict-side-0/tests/task/samples.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Test Samples for the Task Component.""" - -from types import TracebackType -from typing import Self - -from anyio import TASK_STATUS_IGNORED, Condition, Event -from anyio.abc import TaskStatus -from typer import echo - -from grelmicro.sync.abc import Synchronization -from grelmicro.task.abc import Task - -condition = Condition() - - -def test1() -> None: - """Test Function.""" - echo("test1") - - -def test2() -> None: - """Test Function.""" - - -def test3(test: str = "test") -> None: - """Test Function.""" - - -async def notify() -> None: - """Test Function that notifies the condition.""" - async with condition: - condition.notify() - - -async def always_fail() -> None: - """Test Function that always fails.""" - msg = "Test Error" - raise ValueError(msg) - - -class SimpleClass: - """Test Class.""" - - def method(self) -> None: - """Test Method.""" - - @staticmethod - def static_method() -> None: - """Test Static Method.""" - - -class EventTask(Task): - """Test Scheduled Task with Event.""" - - def __init__(self, *, event: Event | None = None) -> None: - """Initialize the event task.""" - self._event = event or Event() - - @property - def name(self) -> str: - """Return the task name.""" - return "event_task" - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run the task that sets the event.""" - task_status.started() - self._event.set() - - -class BadLock(Synchronization): - """Bad Lock.""" - - async def __aenter__(self) -> Self: - """Enter the synchronization primitive.""" - msg = "Bad Lock" - raise ValueError(msg) - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the synchronization primitive.""" diff --git a/.conflict-side-0/tests/task/test_interval.py b/.conflict-side-0/tests/task/test_interval.py deleted file mode 100644 index 308d456..0000000 --- a/.conflict-side-0/tests/task/test_interval.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Test Interval Task.""" - -import pytest -from anyio import create_task_group, sleep, sleep_forever -from pytest_mock import MockFixture - -from grelmicro.task._interval import IntervalTask -from tests.task.samples import ( - BadLock, - always_fail, - condition, - notify, - test1, -) - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] - -INTERVAL = 0.1 -SLEEP = 0.01 - - -def test_interval_task_init() -> None: - """Test Interval Task Initialization.""" - # Act - task = IntervalTask(interval=1, function=test1) - # Assert - assert task.name == "tests.task.samples:test1" - - -def test_interval_task_init_with_name() -> None: - """Test Interval Task Initialization with Name.""" - # Act - task = IntervalTask(interval=1, function=test1, name="test1") - # Assert - assert task.name == "test1" - - -def test_interval_task_init_with_invalid_interval() -> None: - """Test Interval Task Initialization with Invalid Interval.""" - # Act / Assert - with pytest.raises(ValueError, match="Interval must be greater than 0"): - IntervalTask(interval=0, function=test1) - - -async def test_interval_task_start() -> None: - """Test Interval Task Start.""" - # Arrange - task = IntervalTask(interval=1, function=notify) - # Act - async with create_task_group() as tg: - await tg.start(task) - async with condition: - await condition.wait() - tg.cancel_scope.cancel() - - -async def test_interval_task_execution_error( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test Interval Task Execution Error.""" - # Arrange - task = IntervalTask(interval=1, function=always_fail) - # Act - async with create_task_group() as tg: - await tg.start(task) - await sleep(SLEEP) - tg.cancel_scope.cancel() - - # Assert - assert any( - "Task execution error:" in record.message - for record in caplog.records - if record.levelname == "ERROR" - ) - - -async def test_interval_task_synchronization_error( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test Interval Task Synchronization Error.""" - # Arrange - task = IntervalTask(interval=1, function=notify, sync=BadLock()) - - # Act - async with create_task_group() as tg: - await tg.start(task) - await sleep(SLEEP) - tg.cancel_scope.cancel() - - # Assert - assert any( - "Task synchronization error:" in record.message - for record in caplog.records - if record.levelname == "ERROR" - ) - - -async def test_interval_stop( - caplog: pytest.LogCaptureFixture, mocker: MockFixture -) -> None: - """Test Interval Task stop.""" - # Arrange - caplog.set_level("INFO") - - class CustomBaseException(BaseException): - pass - - mocker.patch( - "grelmicro.task._interval.sleep", side_effect=CustomBaseException - ) - task = IntervalTask(interval=1, function=test1) - - async def leader_election_during_runtime_error() -> None: - async with create_task_group() as tg: - await tg.start(task) - await sleep_forever() - - # Act - with pytest.raises(BaseExceptionGroup): - await leader_election_during_runtime_error() - - # Assert - assert any( - "Task stopped:" in record.message - for record in caplog.records - if record.levelname == "INFO" - ) diff --git a/.conflict-side-0/tests/task/test_manager.py b/.conflict-side-0/tests/task/test_manager.py deleted file mode 100644 index 62c9859..0000000 --- a/.conflict-side-0/tests/task/test_manager.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Test Task Manager.""" - -import pytest -from anyio import Event - -from grelmicro.errors import OutOfContextError -from grelmicro.task import TaskManager -from grelmicro.task.errors import TaskAddOperationError -from tests.task.samples import EventTask - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] - - -def test_task_manager_init() -> None: - """Test Task Manager Initialization.""" - # Act - task = EventTask() - app = TaskManager() - app_with_tasks = TaskManager(tasks=[task]) - # Assert - assert app.tasks == [] - assert app_with_tasks.tasks == [task] - - -async def test_task_manager_context() -> None: - """Test Task Manager Context.""" - # Arrange - event = Event() - task = EventTask(event=event) - app = TaskManager(tasks=[task]) - - # Act - event_before = event.is_set() - async with app: - event_in_context = event.is_set() - - # Assert - assert event_before is False - assert event_in_context is True - - -@pytest.mark.parametrize("auto_start", [True, False]) -async def test_task_manager_auto_start_disabled(*, auto_start: bool) -> None: - """Test Task Manager Auto Start Disabled.""" - # Arrange - event = Event() - task = EventTask(event=event) - app = TaskManager(auto_start=auto_start, tasks=[task]) - - # Act - event_before = event.is_set() - async with app: - event_in_context = event.is_set() - - # Assert - assert event_before is False - assert event_in_context is auto_start - - -async def test_task_manager_already_started_error() -> None: - """Test Task Manager Already Started Warning.""" - # Arrange - app = TaskManager() - - # Act / Assert - async with app: - with pytest.raises(TaskAddOperationError): - await app.start() - - -async def test_task_manager_out_of_context_errors() -> None: - """Test Task Manager Out of Context Errors.""" - # Arrange - app = TaskManager() - - # Act / Assert - with pytest.raises(OutOfContextError): - await app.start() - - with pytest.raises(OutOfContextError): - await app.__aexit__(None, None, None) diff --git a/.conflict-side-0/tests/task/test_router.py b/.conflict-side-0/tests/task/test_router.py deleted file mode 100644 index ed30af7..0000000 --- a/.conflict-side-0/tests/task/test_router.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Test Task Router.""" - -from functools import partial - -import pytest - -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.task import TaskRouter -from grelmicro.task._interval import IntervalTask -from grelmicro.task.errors import FunctionTypeError, TaskAddOperationError -from tests.task.samples import EventTask, SimpleClass, test1, test2, test3 - - -def test_router_init() -> None: - """Test Task Router Initialization.""" - # Arrange - custom_task = EventTask() - - # Act - router = TaskRouter() - router_with_task = TaskRouter(tasks=[custom_task]) - - # Assert - assert router.tasks == [] - assert router_with_task.tasks == [custom_task] - - -def test_router_add_task() -> None: - """Test Task Router Add Task.""" - # Arrange - custom_task1 = EventTask() - custom_task2 = EventTask() - router = TaskRouter() - router_with_task = TaskRouter(tasks=[custom_task1]) - - # Act - router.add_task(custom_task1) - router_with_task.add_task(custom_task2) - - # Assert - assert router.tasks == [custom_task1] - assert router_with_task.tasks == [custom_task1, custom_task2] - - -def test_router_include_router() -> None: - """Test Task Router Include Router.""" - # Arrange - custom_task1 = EventTask() - custom_task2 = EventTask() - router = TaskRouter(tasks=[custom_task1]) - router_with_task = TaskRouter(tasks=[custom_task2]) - - # Act - router.include_router(router_with_task) - - # Assert - assert router.tasks == [custom_task1, custom_task2] - - -def test_router_interval() -> None: - """Test Task Router add interval task.""" - # Arrange - task_count = 4 - custom_task = EventTask() - router = TaskRouter(tasks=[custom_task]) - sync = Lock(backend=MemorySyncBackend(), name="testlock") - - # Act - router.interval(name="test1", seconds=10, sync=sync)(test1) - router.interval(name="test2", seconds=20)(test2) - router.interval(seconds=10)(test3) - - # Assert - assert len(router.tasks) == task_count - assert ( - sum(isinstance(task, IntervalTask) for task in router.tasks) - == task_count - 1 - ) - assert router.tasks[0].name == "event_task" - assert router.tasks[1].name == "test1" - assert router.tasks[2].name == "test2" - assert router.tasks[3].name == "tests.task.samples:test3" - - -def test_router_interval_name_generation() -> None: - """Test Task Router Interval Name Generation.""" - # Arrange - router = TaskRouter() - - # Act - router.interval(seconds=10)(test1) - router.interval(seconds=10)(SimpleClass.static_method) - router.interval(seconds=10)(SimpleClass.method) - - # Assert - assert router.tasks[0].name == "tests.task.samples:test1" - assert ( - router.tasks[1].name == "tests.task.samples:SimpleClass.static_method" - ) - assert router.tasks[2].name == "tests.task.samples:SimpleClass.method" - - -def test_router_interval_name_generation_error() -> None: - """Test Task Router Interval Name Generation Error.""" - # Arrange - router = TaskRouter() - test_instance = SimpleClass() - - # Act - with pytest.raises(FunctionTypeError, match="nested function"): - - @router.interval(seconds=10) - def nested_function() -> None: - pass - - with pytest.raises(FunctionTypeError, match="lambda"): - router.interval(seconds=10)(lambda _: None) - - with pytest.raises(FunctionTypeError, match="method"): - router.interval(seconds=10)(test_instance.method) - - with pytest.raises(FunctionTypeError, match="partial()"): - router.interval(seconds=10)(partial(test1)) - - with pytest.raises( - FunctionTypeError, - match="callable without __module__ or __qualname__ attribute", - ): - router.interval(seconds=10)(object()) # type: ignore[arg-type] - - -def test_router_add_task_when_started() -> None: - """Test Task Router Add Task When Started.""" - # Arrange - custom_task = EventTask() - router = TaskRouter() - router.do_mark_as_started() - - # Act - with pytest.raises(TaskAddOperationError): - router.add_task(custom_task) - - -def test_router_include_router_when_started() -> None: - """Test Task Router Include Router When Started.""" - # Arrange - router = TaskRouter() - router.do_mark_as_started() - router_child = TaskRouter() - - # Act - with pytest.raises(TaskAddOperationError): - router.include_router(router_child) - - -def test_router_started_propagation() -> None: - """Test Task Router Started Propagation.""" - # Arrange - router = TaskRouter() - router_child = TaskRouter() - router.include_router(router_child) - - # Act - router_started_before = router.started() - router_child_started_before = router_child.started() - router.do_mark_as_started() - router_started_after = router.started() - router_child_started_after = router_child.started() - - # Assert - assert router_started_before is False - assert router_child_started_before is False - assert router_started_after is True - assert router_child_started_after is True diff --git a/.conflict-side-0/uv.lock b/.conflict-side-0/uv.lock deleted file mode 100644 index ff11a2b..0000000 --- a/.conflict-side-0/uv.lock +++ /dev/null @@ -1,1934 +0,0 @@ -version = 1 -requires-python = ">=3.11" -resolution-markers = [ - "python_full_version < '3.13'", - "python_full_version >= '3.13'", -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.6.2.post1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, -] - -[[package]] -name = "async-timeout" -version = "4.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, -] - -[[package]] -name = "asyncpg" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, - { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, - { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, - { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, - { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, - { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, - { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, - { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, - { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, - { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, - { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, - { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, - { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, - { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, - { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, - { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, - { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, - { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, - { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, - { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, - { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, - { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, - { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, - { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, -] - -[[package]] -name = "babel" -version = "2.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, -] - -[[package]] -name = "backports-tarfile" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, -] - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, - { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, - { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, - { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, - { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, - { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, - { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, - { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, - { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, - { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, - { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, - { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, - { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, - { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, - { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, - { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, - { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, - { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, - { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, - { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, - { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, - { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, - { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, - { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, - { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, - { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, - { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, - { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, - { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - -[[package]] -name = "click" -version = "8.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "coverage" -version = "7.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 }, - { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 }, - { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 }, - { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 }, - { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 }, - { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 }, - { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 }, - { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 }, - { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 }, - { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 }, - { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 }, - { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 }, - { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 }, - { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 }, - { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 }, - { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 }, - { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, - { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, - { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, - { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, - { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, - { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, - { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, - { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, - { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, - { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, - { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, - { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, - { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, - { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, - { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, - { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, - { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, - { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, - { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, - { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, - { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, - { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, - { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[package]] -name = "cryptography" -version = "43.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, - { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, - { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, - { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, - { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, - { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, - { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, - { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, - { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, - { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, - { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, - { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, -] - -[[package]] -name = "cyclic" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547 }, -] - -[[package]] -name = "distlib" -version = "0.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, -] - -[[package]] -name = "docker" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "requests" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, -] - -[[package]] -name = "fast-depends" -version = "2.4.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/f5/8b42b7588a67ad78991e5e7ca0e0c6a1ded535a69a725e4e48d3346a20c1/fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d", size = 16682 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/08/4adb160d8394053289fdf3b276e93b53271fd463e54fff8911b23c1db4ed/fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2", size = 17651 }, -] - -[[package]] -name = "fastapi" -version = "0.115.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/29/f71316b9273b6552a263748e49cd7b83898dc9499a663d30c7b9cb853cb8/fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", size = 301047 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/c4/148d5046a96c428464557264877ae5a9338a83bbe0df045088749ec89820/fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796", size = 94866 }, -] - -[[package]] -name = "fastapi-cli" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/1ad5ce32d029aeb9117e9a5a9b3e314a8477525d60c12a9b7730a3c186ec/fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", size = 15571 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/ea/4b5011012ac925fe2f83b19d0e09cee9d324141ec7bf5e78bb2817f96513/fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46", size = 9489 }, -] - -[[package]] -name = "faststream" -version = "0.5.30" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "fast-depends" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/d3/c2a3e1233274c93a4978cbac210a81ba05cee09e2e0051049b40f55406f1/faststream-0.5.30.tar.gz", hash = "sha256:50ad5288719cfa75c13e9c277d40afae62533a590facad6e6d215e868f2b97f4", size = 284478 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/ce/a9eec6c2c9803de6bc2b2a5cac35d56b8908c64fcdd4c73616c1a16c9b90/faststream-0.5.30-py3-none-any.whl", hash = "sha256:bf48826be99210f3e9c7dff1b2a17b4bc4762c873c5558ac81b9b873549ae6a1", size = 382011 }, -] - -[[package]] -name = "filelock" -version = "3.16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, -] - -[[package]] -name = "grelmicro" -version = "0.2.2" -source = { editable = "." } -dependencies = [ - { name = "anyio" }, - { name = "fast-depends" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, -] - -[package.optional-dependencies] -postgres = [ - { name = "asyncpg" }, -] -redis = [ - { name = "redis" }, -] -standard = [ - { name = "loguru" }, - { name = "orjson" }, -] - -[package.dev-dependencies] -dev = [ - { name = "fastapi" }, - { name = "fastapi-cli" }, - { name = "faststream" }, - { name = "hatch" }, - { name = "mdx-include" }, - { name = "mypy" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "pytest-randomly" }, - { name = "pytest-timeout" }, - { name = "ruff" }, - { name = "testcontainers", extra = ["redis"] }, -] -docs = [ - { name = "mkdocs-material" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, -] - -[package.metadata] -requires-dist = [ - { name = "anyio", specifier = ">=4.0.0" }, - { name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.30.0" }, - { name = "fast-depends", specifier = ">=2.0.0" }, - { name = "loguru", marker = "extra == 'standard'", specifier = ">=0.7.2" }, - { name = "orjson", marker = "extra == 'standard'", specifier = ">=3.10.11" }, - { name = "pydantic", specifier = ">=2.5.0" }, - { name = "pydantic-settings", specifier = ">=2.5.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "fastapi", specifier = ">=0.115.5" }, - { name = "fastapi-cli", specifier = ">=0.0.5" }, - { name = "faststream", specifier = ">=0.5.30" }, - { name = "hatch", specifier = ">=1.13.0" }, - { name = "mdx-include", specifier = ">=1.4.2" }, - { name = "mypy", specifier = ">=1.12.0" }, - { name = "pre-commit", specifier = ">=4.0.1" }, - { name = "pytest", specifier = ">=8.0.0" }, - { name = "pytest-cov", specifier = ">=6.0.0" }, - { name = "pytest-mock", specifier = ">=3.14.0" }, - { name = "pytest-randomly", specifier = ">=3.16.0" }, - { name = "pytest-timeout", specifier = ">=2.3.1" }, - { name = "ruff", specifier = ">=0.7.4" }, - { name = "testcontainers", extras = ["postgres", "redis"], specifier = ">=4.8.2" }, -] -docs = [ - { name = "mkdocs-material", specifier = ">=9.5.44" }, - { name = "pygments", specifier = ">=2.18.0" }, - { name = "pymdown-extensions", specifier = ">=10.12" }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, -] - -[[package]] -name = "hatch" -version = "1.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "hatchling" }, - { name = "httpx" }, - { name = "hyperlink" }, - { name = "keyring" }, - { name = "packaging" }, - { name = "pexpect" }, - { name = "platformdirs" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "tomli-w" }, - { name = "tomlkit" }, - { name = "userpath" }, - { name = "uv" }, - { name = "virtualenv" }, - { name = "zstandard" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/ed/5001de278f8d7381cbc84f5efdae72308fe37493bc063878f6a1ac07dab8/hatch-1.13.0.tar.gz", hash = "sha256:5e1a75770cfe8f3ebae3abfded3a976238b0acefd19cdabc5245597525b8066f", size = 5188060 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/8d/6d965a22bc38cec091ba82131624bb5d75471094d7fe05e829536de3de2f/hatch-1.13.0-py3-none-any.whl", hash = "sha256:bb1a18558a626279cae338b4d8a9d3ca4226d5e06d50de600608c57acd131b67", size = 125757 }, -] - -[[package]] -name = "hatchling" -version = "1.26.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "pathspec" }, - { name = "pluggy" }, - { name = "trove-classifiers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/47/7ec270a9567262ae3cb32dd420d2b53bf7aee769aca1f240eae0426b5bbc/hatchling-1.26.3.tar.gz", hash = "sha256:b672a9c36a601a06c4e88a1abb1330639ee8e721e0535a37536e546a667efc7a", size = 54968 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl", hash = "sha256:c407e1c6c17b574584a66ae60e8e9a01235ecb6dc61d01559bb936577aaf5846", size = 75773 }, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, -] - -[[package]] -name = "httptools" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, - { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, - { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, - { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, - { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, - { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, - { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, - { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, - { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, - { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, - { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, - { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, - { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, -] - -[[package]] -name = "httpx" -version = "0.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, -] - -[[package]] -name = "hyperlink" -version = "21.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, -] - -[[package]] -name = "identify" -version = "2.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/79/7a520fc5011e02ca3f3285b5f6820eaf80443eb73e3733f73c02fb42ba0b/identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd", size = 99113 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/86/c4395700f3c5475424fb5c41e20c16be28d10c904aee4d005ba3217fc8e7/identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", size = 98982 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, -] - -[[package]] -name = "jaraco-classes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, -] - -[[package]] -name = "jaraco-context" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, -] - -[[package]] -name = "jaraco-functools" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, -] - -[[package]] -name = "jeepney" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, -] - -[[package]] -name = "jinja2" -version = "3.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, -] - -[[package]] -name = "keyring" -version = "25.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, - { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "secretstorage", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/24/64447b13df6a0e2797b586dad715766d756c932ce8ace7f67bd384d76ae0/keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6", size = 62675 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/c9/353c156fa2f057e669106e5d6bcdecf85ef8d3536ce68ca96f18dc7b6d6f/keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741", size = 39096 }, -] - -[[package]] -name = "loguru" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549 }, -] - -[[package]] -name = "markdown" -version = "3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "mdx-include" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cyclic" }, - { name = "markdown" }, - { name = "rcslice" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591 }, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, -] - -[[package]] -name = "mkdocs-material" -version = "9.5.44" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/56/182d8121db9ab553cdf9bc58d5972b89833f60b63272f693c1f2b849b640/mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0", size = 3964306 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/eb/a801d00e0e210d82184aacce596906ec065422c78a7319244ba0771c4ded/mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca", size = 8674509 }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, -] - -[[package]] -name = "more-itertools" -version = "10.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, -] - -[[package]] -name = "mypy" -version = "1.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/03/744330105a74dc004578f47ec27e1bf66b1dd5664ea444d18423e41343bd/mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d", size = 3150767 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/0a/70de7c97a86cb85535077ab5cef1cbc4e2812fd2e9cc21d78eb561a6b80f/mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", size = 10940998 }, - { url = "https://files.pythonhosted.org/packages/c0/97/9ed6d4834d7549936ab88533b302184fb568a0940c4000d2aaee6dc07112/mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", size = 10108523 }, - { url = "https://files.pythonhosted.org/packages/48/41/1686f37d09c915dfc5b683e20cc99dabac199900b5ca6d22747b99ddcb50/mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", size = 12505553 }, - { url = "https://files.pythonhosted.org/packages/8d/2b/2dbcaa7e97b23f27ced77493256ee878f4a140ac750e198630ff1b9b60c6/mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", size = 12988634 }, - { url = "https://files.pythonhosted.org/packages/54/55/710d082e91a2ccaea21214229b11f9215a9d22446f949491b5457655e82b/mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", size = 9630747 }, - { url = "https://files.pythonhosted.org/packages/8a/74/b9e0e4f06e951e277058f878302faa154d282ca11274c59fe08353f52949/mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", size = 11079902 }, - { url = "https://files.pythonhosted.org/packages/9f/62/fcad290769db3eb0de265094cef5c94d6075c70bc1e42b67eee4ca192dcc/mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", size = 10072373 }, - { url = "https://files.pythonhosted.org/packages/cb/27/9ac78349c2952e4446288ec1174675ab9e0160ed18c2cb1154fa456c54e8/mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", size = 12589779 }, - { url = "https://files.pythonhosted.org/packages/7c/4a/58cebd122cf1cba95680ac51303fbeb508392413ca64e3e711aa7d4877aa/mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", size = 13044459 }, - { url = "https://files.pythonhosted.org/packages/5b/c7/672935e2a3f9bcc07b1b870395a653f665657bef3cdaa504ad99f56eadf0/mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", size = 9731919 }, - { url = "https://files.pythonhosted.org/packages/bb/b0/092be5094840a401940c95224f63bb2a8f09bce9251ac1df180ec523830c/mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", size = 11068611 }, - { url = "https://files.pythonhosted.org/packages/9a/86/f20f53b8f062876c39602243d7a59b5cabd6b24315d8de511d607fa4de6a/mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", size = 10068036 }, - { url = "https://files.pythonhosted.org/packages/84/c7/1dbd6575785522da1d4c1ac2c419505fcf23bee74811880cac447a4a77ab/mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", size = 12585671 }, - { url = "https://files.pythonhosted.org/packages/46/8a/f6ae18b446eb2bccce54c4bd94065bcfe417d6c67021dcc032bf1e720aff/mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", size = 13036083 }, - { url = "https://files.pythonhosted.org/packages/59/e6/fc65fde3dc7156fce8d49ba21c7b1f5d866ad50467bf196ca94a7f6d2c9e/mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", size = 9735467 }, - { url = "https://files.pythonhosted.org/packages/84/6b/1db9de4e0764778251fb2d64cb7455cf6db75dc99c9f72c8b7e74b6a8a17/mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", size = 2646060 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, -] - -[[package]] -name = "orjson" -version = "3.10.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/3a/10320029954badc7eaa338a15ee279043436f396e965dafc169610e4933f/orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", size = 5444879 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/25/c869a1fbd481dcb02c70032fd6a7243de7582bc48c7cae03d6f0985a11c0/orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6", size = 266432 }, - { url = "https://files.pythonhosted.org/packages/6a/a4/2307155ee92457d28345308f7d8c0e712348404723025613adeffcb531d0/orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe", size = 151884 }, - { url = "https://files.pythonhosted.org/packages/aa/82/daf1b2596dd49fe44a1bd92367568faf6966dcb5d7f99fd437c3d0dc2de6/orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67", size = 167371 }, - { url = "https://files.pythonhosted.org/packages/63/a8/680578e4589be5fdcfe0186bdd7dc6fe4a39d30e293a9da833cbedd5a56e/orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b", size = 154368 }, - { url = "https://files.pythonhosted.org/packages/6e/ce/9cb394b5b01ef34579eeca6d704b21f97248f607067ce95a24ba9ea2698e/orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d", size = 165725 }, - { url = "https://files.pythonhosted.org/packages/49/24/55eeb05cfb36b9e950d05743e6f6fdb7d5f33ca951a27b06ea6d03371aed/orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5", size = 142522 }, - { url = "https://files.pythonhosted.org/packages/94/0c/3a6a289e56dcc9fe67dc6b6d33c91dc5491f9ec4a03745efd739d2acf0ff/orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a", size = 146934 }, - { url = "https://files.pythonhosted.org/packages/1d/5c/a08c0e90a91e2526029a4681ff8c6fc4495b8bab77d48801144e378c7da9/orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981", size = 142904 }, - { url = "https://files.pythonhosted.org/packages/2c/c9/710286a60b14e88288ca014d43befb08bb0a4a6a0f51b875f8c2f05e8205/orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55", size = 144459 }, - { url = "https://files.pythonhosted.org/packages/7d/68/ef7b920e0a09e02b1a30daca1b4864938463797995c2fabe457c1500220a/orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec", size = 136444 }, - { url = "https://files.pythonhosted.org/packages/78/f2/a712dbcef6d84ff53e13056e7dc69d9d4844bd1e35e51b7431679ddd154d/orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51", size = 266505 }, - { url = "https://files.pythonhosted.org/packages/94/54/53970831786d71f98fdc13c0f80451324c9b5c20fbf42f42ef6147607ee7/orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97", size = 151745 }, - { url = "https://files.pythonhosted.org/packages/35/38/482667da1ca7ef95d44d4d2328257a144fd2752383e688637c53ed474d2a/orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19", size = 167274 }, - { url = "https://files.pythonhosted.org/packages/23/2f/5bb0a03e819781d82dadb733fde8ebbe20d1777d1a33715d45ada4d82ce8/orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0", size = 154605 }, - { url = "https://files.pythonhosted.org/packages/49/e9/14cc34d45c7bd51665aff9b1bb6b83475a61c52edb0d753fffe1adc97764/orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433", size = 165874 }, - { url = "https://files.pythonhosted.org/packages/7b/61/c2781ecf90f99623e97c67a31e8553f38a1ecebaf3189485726ac8641576/orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5", size = 142813 }, - { url = "https://files.pythonhosted.org/packages/4d/4f/18c83f78b501b6608569b1610fcb5a25c9bb9ab6a7eb4b3a55131e0fba37/orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd", size = 146762 }, - { url = "https://files.pythonhosted.org/packages/ba/19/ea80d5b575abd3f76a790409c2b7b8a60f3fc9447965c27d09613b8bddf4/orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b", size = 143186 }, - { url = "https://files.pythonhosted.org/packages/2c/f5/d835fee01a0284d4b78effc24d16e7609daac2ff6b6851ca1bdd3b6194fc/orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d", size = 144489 }, - { url = "https://files.pythonhosted.org/packages/03/60/748e0e205060dec74328dfd835e47902eb5522ae011766da76bfff64e2f4/orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284", size = 136614 }, - { url = "https://files.pythonhosted.org/packages/13/92/400970baf46b987c058469e9e779fb7a40d54a5754914d3634cca417e054/orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", size = 266402 }, - { url = "https://files.pythonhosted.org/packages/3c/fa/f126fc2d817552bd1f67466205abdcbff64eab16f6844fe6df2853528675/orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", size = 140826 }, - { url = "https://files.pythonhosted.org/packages/ad/18/9b9664d7d4af5b4fe9fe6600b7654afc0684bba528260afdde10c4a530aa/orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", size = 142593 }, - { url = "https://files.pythonhosted.org/packages/20/f9/a30c68f12778d5e58e6b5cdd26f86ee2d0babce1a475073043f46fdd8402/orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", size = 146777 }, - { url = "https://files.pythonhosted.org/packages/f2/97/12047b0c0e9b391d589fb76eb40538f522edc664f650f8e352fdaaf77ff5/orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", size = 142961 }, - { url = "https://files.pythonhosted.org/packages/a4/97/d904e26c1cabf2dd6ab1b0909e9b790af28a7f0fcb9d8378d7320d4869eb/orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", size = 144486 }, - { url = "https://files.pythonhosted.org/packages/42/62/3760bd1e6e949321d99bab238d08db2b1266564d2f708af668f57109bb36/orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", size = 136361 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, -] - -[[package]] -name = "pre-commit" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - -[[package]] -name = "pydantic" -version = "2.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, -] - -[[package]] -name = "pydantic-core" -version = "2.23.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, - { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, - { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, - { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, - { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, - { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, - { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, - { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, - { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, - { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, - { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, - { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, - { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, - { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, - { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, - { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, - { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, - { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, - { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, - { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, - { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, - { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, - { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, - { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, - { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, - { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, - { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, - { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, - { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, - { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, - { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, - { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, - { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, - { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, - { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, - { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, -] - -[[package]] -name = "pygments" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/32f05854cfd432e9286bb41a870e0d1a926b72df5f5cdb6dec962b2e369e/pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7", size = 840790 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/32/95a164ddf533bd676cbbe878e36e89b4ade3efde8dd61d0148c90cbbe57e/pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", size = 263448 }, -] - -[[package]] -name = "pytest" -version = "8.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, -] - -[[package]] -name = "pytest-cov" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, -] - -[[package]] -name = "pytest-mock" -version = "3.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, -] - -[[package]] -name = "pytest-randomly" -version = "3.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396 }, -] - -[[package]] -name = "pytest-timeout" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - -[[package]] -name = "pywin32" -version = "308" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156 }, - { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559 }, - { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495 }, - { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, - { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, - { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, - { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, - { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, - { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, -] - -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, -] - -[[package]] -name = "rcslice" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180 }, -] - -[[package]] -name = "redis" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, -] - -[[package]] -name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "rich" -version = "13.9.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", size = 222889 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", size = 242157 }, -] - -[[package]] -name = "ruff" -version = "0.7.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, - { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, - { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, - { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, - { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, - { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, - { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, - { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, - { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, - { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, - { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, - { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, - { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, - { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, - { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, - { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, - { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, -] - -[[package]] -name = "secretstorage" -version = "3.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - -[[package]] -name = "six" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "starlette" -version = "0.41.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, -] - -[[package]] -name = "testcontainers" -version = "4.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docker" }, - { name = "typing-extensions" }, - { name = "urllib3" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/72/c58d84f5704c6caadd9f803a3adad5ab54ac65328c02d13295f40860cf33/testcontainers-4.8.2.tar.gz", hash = "sha256:dd4a6a2ea09e3c3ecd39e180b6548105929d0bb78d665ce9919cb3f8c98f9853", size = 63590 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/77/5ac0dff2903a033d83d971fd85957356abdb66a327f3589df2b3d1a586b4/testcontainers-4.8.2-py3-none-any.whl", hash = "sha256:9e19af077cd96e1957c13ee466f1f32905bc6c5bc1bc98643eb18be1a989bfb0", size = 104326 }, -] - -[package.optional-dependencies] -redis = [ - { name = "redis" }, -] - -[[package]] -name = "tomli" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, -] - -[[package]] -name = "tomli-w" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, -] - -[[package]] -name = "tomlkit" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, -] - -[[package]] -name = "trove-classifiers" -version = "2024.10.21.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/85/92c2667cf221b37648041ce9319427f92fa76cbec634aad844e67e284706/trove_classifiers-2024.10.21.16.tar.gz", hash = "sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3", size = 16153 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl", hash = "sha256:0fb11f1e995a757807a8ef1c03829fbd4998d817319abcef1f33165750f103be", size = 13546 }, -] - -[[package]] -name = "typer" -version = "0.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] - -[[package]] -name = "userpath" -version = "1.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065 }, -] - -[[package]] -name = "uv" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/ad/66cc8e00c217e7fcf76598c880632b480aa38d4cad311596b78e99737498/uv-0.5.4.tar.gz", hash = "sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00", size = 2315678 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/3e/6bf24d7bb0d11715ea783ecabcacdecdc8c51fca0144fcdad2090d65bae5/uv-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2", size = 13853445 }, - { url = "https://files.pythonhosted.org/packages/b8/be/c3acbe2944cd694a5d61a7a461468fa886512c84014545bb8f3244092eaa/uv-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58", size = 13969300 }, - { url = "https://files.pythonhosted.org/packages/1f/c5/06e3b93045179b92d75cf94e6e224baec3226070f1cbc0e11d4898300b54/uv-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4", size = 12932325 }, - { url = "https://files.pythonhosted.org/packages/b8/f9/06ab86e9f0c270c495077ef2b588458172ed84f9c337de725c8b08872354/uv-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39", size = 13183356 }, - { url = "https://files.pythonhosted.org/packages/c1/cb/bee01ef23e5020dc1f12d86ca8f82e95a723585db3ec64bfab4016e5616c/uv-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6", size = 13622310 }, - { url = "https://files.pythonhosted.org/packages/19/4b/128fd874151919c71af51f528db28964e6d8e509fff12210ec9ba99b13fb/uv-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263", size = 14207832 }, - { url = "https://files.pythonhosted.org/packages/b1/2b/0fed8a49440494f6806dcb67021ca8f14d46f45a665235fc153791e19574/uv-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83", size = 14878796 }, - { url = "https://files.pythonhosted.org/packages/c9/35/a6dc404d4d8884e26ad7bda004c101972fe7d81f86546a8628272812b897/uv-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25", size = 14687838 }, - { url = "https://files.pythonhosted.org/packages/74/9e/c2ebf66b90d48def06cda29626bb38068418ed135ca903beb293825ef66d/uv-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927", size = 18960541 }, - { url = "https://files.pythonhosted.org/packages/3d/67/28a8b4c23920ae1b1b0103ebae2fa176bd5677c4353b5e814a51bd183285/uv-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194", size = 14471756 }, - { url = "https://files.pythonhosted.org/packages/e9/1c/9698818f4c5493dfd5ab0899a90eee789cac214de2f171220bcdfaefc93a/uv-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01", size = 13389089 }, - { url = "https://files.pythonhosted.org/packages/0b/30/31a9985d84ffb63fb9212fa2b565497e0ceb581be055e5cc760afbe26b11/uv-0.5.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c", size = 13612748 }, - { url = "https://files.pythonhosted.org/packages/26/8d/bae613187ba88d74f0268246ce140f23d399bab96d2cbc055d6e4adafd09/uv-0.5.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876", size = 13946421 }, - { url = "https://files.pythonhosted.org/packages/0e/22/efd1eec81a566139bced68f4bd140c275edac3dac1bd6236cf8d756423db/uv-0.5.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119", size = 15752913 }, - { url = "https://files.pythonhosted.org/packages/49/b2/0cc4ae143b9605c25e75772aea22876b5875db79982ba62bb6f8d3099fab/uv-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc", size = 14599503 }, - { url = "https://files.pythonhosted.org/packages/51/9a/33d40a5068fd37c4f7b4fa82396e3ee90a691cd256f364ff398612c1d5d4/uv-0.5.4-py3-none-win32.whl", hash = "sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa", size = 13749570 }, - { url = "https://files.pythonhosted.org/packages/b1/c8/827e4da65cbdab2c1619767a68ab99a31de078e511b71ca9f24777df33f9/uv-0.5.4-py3-none-win_amd64.whl", hash = "sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb", size = 15573613 }, -] - -[[package]] -name = "uvicorn" -version = "0.32.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, -] - -[[package]] -name = "virtualenv" -version = "20.27.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838 }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, -] - -[[package]] -name = "watchfiles" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, - { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, - { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, - { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, - { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, - { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, - { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, - { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, - { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, - { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, - { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, - { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, - { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, - { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, - { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, - { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, - { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, - { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, - { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, - { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, - { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, - { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, - { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, - { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, - { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, - { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, - { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, - { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, - { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, - { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, - { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, - { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, - { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, - { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, - { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, - { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, - { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, - { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, -] - -[[package]] -name = "websockets" -version = "14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, - { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, - { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, - { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, - { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, - { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, - { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, - { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, - { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, - { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, - { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, - { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, - { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, - { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, - { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, - { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, - { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, - { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, - { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, - { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, - { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, - { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, - { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, - { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, - { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, - { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, - { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, - { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, - { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, - { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, - { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, - { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, - { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, - { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, -] - -[[package]] -name = "win32-setctime" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", size = 3676 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad", size = 3604 }, -] - -[[package]] -name = "wrapt" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313 }, - { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164 }, - { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890 }, - { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118 }, - { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746 }, - { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668 }, - { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556 }, - { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712 }, - { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327 }, - { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523 }, - { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614 }, - { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316 }, - { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322 }, - { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055 }, - { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291 }, - { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374 }, - { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896 }, - { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738 }, - { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568 }, - { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653 }, - { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, -] - -[[package]] -name = "zipp" -version = "3.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, -] - -[[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 }, -] diff --git a/.conflict-side-1/.github/workflows/ci.yml b/.conflict-side-1/.github/workflows/ci.yml deleted file mode 100644 index 5fb99bb..0000000 --- a/.conflict-side-1/.github/workflows/ci.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: CI - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -jobs: - lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras - - - name: Run Mypy - run: uv run mypy . - - test: - name: Test Python ${{ matrix.python }} - runs-on: "ubuntu-latest" - strategy: - fail-fast: true - matrix: - python: ["3.11", "3.12", "3.13"] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras --python ${{ matrix.python }} - - - name: Run unit tests - run: uv run pytest -x - - - name: Run integration tests - run: uv run pytest -x -m integration --cov-append - - - name: Rename coverage report - run: mv .coverage .coverage.py${{ matrix.python }} - - - name: Save coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-${{ matrix.python }} - path: .coverage.py${{ matrix.python }} - include-hidden-files: true - - coverage-report: - name: Coverage report - runs-on: ubuntu-latest - needs: [test] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Get coverage reports - uses: actions/download-artifact@v4 - with: - pattern: coverage-* - merge-multiple: true - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --all-extras - - - name: Combine coverage reports - run: | - uv run coverage combine .coverage.* - uv run coverage xml -o cov.xml - - - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v4.0.1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./cov.xml diff --git a/.conflict-side-1/.github/workflows/release.yml b/.conflict-side-1/.github/workflows/release.yml deleted file mode 100644 index c8d4bab..0000000 --- a/.conflict-side-1/.github/workflows/release.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Release - -on: - release: - types: - - published - -jobs: - bump-version: - name: Bump version - runs-on: ubuntu-latest - steps: - - - name: Generate GitHub App Token - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ secrets.GRELINFO_ID }} - private-key: ${{ secrets.GRELINFO_KEY }} - - - name: Get GitHub App User ID - id: user-id - run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - - - name: Configure Git App Credentials - run: | - git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' - git config --global user.email '${{ steps.user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}@users.noreply.github.com>' - - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref_name }} - token: ${{ steps.app-token.outputs.token }} - persist-credentials: false - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Get release version - id: release-version - run: echo "release-version=${GITHUB_REF#refs/*/}" >> "$GITHUB_OUTPUT" - - - name: Get current version - id: current-version - run: echo "current-version=$(uv run hatch version)" >> "$GITHUB_OUTPUT" - - - name: Bump version if necessary - if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} - run: | - uv run hatch version $RELEASE_VERSION - uv lock - - - name: Commit and push changes - if: ${{ steps.release-version.outputs.release-version != steps.current-version.outputs.current-version }} - run: | - git add . - git commit -m "🚀 Release $RELEASE_VERSION" - git tag -f $RELEASE_VERSION - git push origin $RELEASE_VERSION --force - git push origin HEAD:main - - publish-docs: - runs-on: ubuntu-latest - needs: [bump-version] - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.ref_name }} - - - name: Configure Git Credentials - run: | - git config user.name "${GITHUB_ACTOR}" - git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Install dependencies - run: uv sync --group docs - - - name: Deploy docs on GitHub Pages - run: uv run mkdocs gh-deploy --force - - publish-pypi: - needs: [bump-version] - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ github.ref_name }} - - - name: Install uv - uses: astral-sh/setup-uv@v3 - with: - enable-cache: true - - - name: Build - run: uv build - - - name: Publish - run: uv publish -t ${{ secrets.PYPI_TOKEN }} diff --git a/.conflict-side-1/.gitignore b/.conflict-side-1/.gitignore deleted file mode 100644 index 0d118ab..0000000 --- a/.conflict-side-1/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -# Python-generated files -__pycache__/ -*.py[oc] -build/ -dist/ -wheels/ -*.egg-info - -# Virtual environments -.venv - -# Coverage -cov.xml -.coverage - -# Mkdocs -site/ diff --git a/.conflict-side-1/.pre-commit-config.yaml b/.conflict-side-1/.pre-commit-config.yaml deleted file mode 100644 index 5e5a141..0000000 --- a/.conflict-side-1/.pre-commit-config.yaml +++ /dev/null @@ -1,63 +0,0 @@ -default_language_version: - python: python3.11 - -repos: - -- repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: end-of-file-fixer - - id: check-toml - - id: check-yaml - - id: check-added-large-files - - id: trailing-whitespace - -- repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.0 - hooks: - - id: ruff - args: [ --fix ] - - id: ruff-format - -- repo: https://github.com/codespell-project/codespell - rev: v2.3.0 - hooks: - - id: codespell - -- repo: local - hooks: - - - id: readme-to-docs - name: readme-to-docs - description: "Copy README.md to docs/index.md" - entry: cp README.md docs/index.md - language: system - pass_filenames: false - - # --- Local development hooks --- - - id: uv-lock - name: uv-lock - description: "Lock dependencies with 'uv lock'" - entry: uv lock - language: system - pass_filenames: false - - - id: mypy - name: mypy - description: "Run 'mypy' for static type checking" - entry: uv run mypy - language: system - types: [python] - require_serial: true - - - id: pytest - name: pytest - description: "Run 'pytest' for unit testing" - entry: uv run pytest --cov-fail-under=90 - language: system - pass_filenames: false - -ci: - autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks - autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate - skip: [uv-lock, mypy, pytest] diff --git a/.conflict-side-1/.vscode/settings.json b/.conflict-side-1/.vscode/settings.json deleted file mode 100644 index 806ffc4..0000000 --- a/.conflict-side-1/.vscode/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - // Editor settings - "editor.rulers": [80, 100], - "files.trimTrailingWhitespace": true, - "terminal.integrated.scrollback": 10000, - - // Files exclude settings - "files.exclude": { - "**/.git": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "**/__pycache__": true, - "**/.venv": true, - "**/.mypy_cache": true, - "**/.pytest_cache": true, - "**/.ruff_cache": true, - ".coverage": true - }, - - // Python settings - "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", - "python.terminal.activateEnvInCurrentTerminal": true, - "python.terminal.activateEnvironment": true, - "python.testing.pytestEnabled": true, - "python.testing.unittestEnabled": false, - "python.testing.pytestArgs": ["--no-cov", "--color=yes"], - "python.analysis.inlayHints.pytestParameters": true, - - // Python editor settings - "[python]": { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff" - }, - - // Mypy settings - "mypy-type-checker.importStrategy": "fromEnvironment", - - // YAML settings - "yaml.schemas": { - "https://squidfunk.github.io/mkdocs-material/schema.json": "mkdocs.yml" - }, - "yaml.customTags": [ - "!ENV scalar", - "!ENV sequence", - "!relative scalar", - "tag:yaml.org,2002:python/name:material.extensions.emoji.to_svg", - "tag:yaml.org,2002:python/name:material.extensions.emoji.twemoji", - "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", - "tag:yaml.org,2002:python/object/apply:pymdownx.slugs.slugify mapping" - ], - - // Ruff settings - "ruff.configurationPreference": "filesystemFirst" -} diff --git a/.conflict-side-1/LICENSE b/.conflict-side-1/LICENSE deleted file mode 100644 index 18dafa2..0000000 --- a/.conflict-side-1/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Loïc Gremaud - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/.conflict-side-1/README.md b/.conflict-side-1/README.md deleted file mode 100644 index 9f3e0ff..0000000 --- a/.conflict-side-1/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# Grelmicro - -Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. - -It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. - -[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) -[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) -[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) - -______________________________________________________________________ - -**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) - -**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) - -______________________________________________________________________ - -## Overview - -Grelmicro provides essential features for building robust distributed systems, including: - -- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. -- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. -- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. -- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. - -These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. - -### Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. - -### Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. - -The available primitives are: - -- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **Lock**: A distributed lock that can be used to synchronize access to shared resources. - -### Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Installation - -```bash -pip install grelmicro -``` - -## Examples - -### FastAPI Integration - -- Create a file `main.py` with: - -```python -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") -``` - -## Dependencies - -Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. - -### `standard` Dependencies - -When you install Grelmicro with `pip install grelmicro[standard]` it comes with: - -- `loguru`: A Python logging library. -- `orjson`: A fast, correct JSON library for Python. - -### `redis` Dependencies - -When you install Grelmicro with `pip install grelmicro[redis]` it comes with: - -- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). - -### `postgres` Dependencies - -When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: - -- `asyncpg`: The Python `asyncio` interface for PostgreSQL. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/.conflict-side-1/docs/index.md b/.conflict-side-1/docs/index.md deleted file mode 100644 index 9f3e0ff..0000000 --- a/.conflict-side-1/docs/index.md +++ /dev/null @@ -1,158 +0,0 @@ -# Grelmicro - -Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. - -It is the perfect companion for building cloud-native applications with FastAPI and FastStream, providing essential tools for running in distributed and containerized environments. - -[![PyPI - Version](https://img.shields.io/pypi/v/grelmicro)](https://pypi.org/project/grelmicro/) -[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/grelmicro)](https://pypi.org/project/grelmicro/) -[![codecov](https://codecov.io/gh/grelinfo/grelmicro/graph/badge.svg?token=GDFY0AEFWR)](https://codecov.io/gh/grelinfo/grelmicro) -[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) - -______________________________________________________________________ - -**Documentation**: [https://grelmicro.grel.info](https://grelmicro.grel.info) - -**Source Code**: [https://github.com/grelinfo/grelmicro](https://github.com/grelinfo/grelmicro) - -______________________________________________________________________ - -## Overview - -Grelmicro provides essential features for building robust distributed systems, including: - -- **Backends**: Technology-agnostic design supporting Redis, PostgreSQL, and in-memory backends for testing. -- **Logging**: Easy-to-configure logging with support of both text or JSON structured format. -- **Task Scheduler**: A simple and efficient task scheduler for running periodic tasks. -- **Synchronization Primitives**: Includes leader election and distributed lock mechanisms. - -These features address common challenges in microservices and distributed, containerized systems while maintaining ease of use. - -### Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to `stdout`. It supports JSON formatting and allows log level configuration via environment variables. - -### Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends like Redis, PostgreSQL, and in-memory for testing. - -The available primitives are: - -- **Leader Election**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **Lock**: A distributed lock that can be used to synchronize access to shared resources. - -### Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Installation - -```bash -pip install grelmicro -``` - -## Examples - -### FastAPI Integration - -- Create a file `main.py` with: - -```python -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") -``` - -## Dependencies - -Grelmicro depends on Pydantic v2+, AnyIO v4+, and FastDepends. - -### `standard` Dependencies - -When you install Grelmicro with `pip install grelmicro[standard]` it comes with: - -- `loguru`: A Python logging library. -- `orjson`: A fast, correct JSON library for Python. - -### `redis` Dependencies - -When you install Grelmicro with `pip install grelmicro[redis]` it comes with: - -- `redis-py`: The Python interface to the Redis key-value store (the async interface depends on `asyncio`). - -### `postgres` Dependencies - -When you install Grelmicro with `pip install grelmicro[postgres]` it comes with: - -- `asyncpg`: The Python `asyncio` interface for PostgreSQL. - -## License - -This project is licensed under the terms of the MIT license. diff --git a/.conflict-side-1/docs/logging.md b/.conflict-side-1/docs/logging.md deleted file mode 100644 index 4575b03..0000000 --- a/.conflict-side-1/docs/logging.md +++ /dev/null @@ -1,73 +0,0 @@ -# Logging - -The `logging` package provides a simple and easy-to-configure logging system. - -The logging feature adheres to the 12-factor app methodology, directing logs to stdout. It supports JSON formatting and allows log level configuration via environment variables. - -## Dependencies - -For the moment the `logging` package is only working with the `loguru` Python logging library. -When `orjson` is installed, it will be used as the default JSON serializer for faster performance, otherwise, the standard `json` library will be used. - -[**Loguru**](https://loguru.readthedocs.io/en/stable/overview.html) is used as the logging library. - -For using `logging` package, please install the required dependencies: - -=== "Standard" - ```bash - pip install grelmicro[standard] - ``` - -=== "only loguru (minimum)" - ```bash - pip install loguru - ``` - -=== "loguru and orjson (manual)" - ```bash - pip install loguru orjson - ``` - - -## Configure Logging - -Just call the `configure_logging` function to set up the logging system. - -```python -{!> ../examples/logging/configure_logging.py!} -``` - -### Settings - -You can change the default settings using the following environment variables: - -- `LOG_LEVEL`: Set the desired log level (default: `INFO`). -- `LOG_FORMAT`: Choose the log format. Options are `TEXT` and `JSON`, or you can provide a custom [loguru](https://loguru.readthedocs.io/en/stable/overview.html) template (default: `TEXT`). - - -## Examples - -### Basic Usage - -Here is a quick example of how to use the logging system: - -```python -{!> ../examples/logging/basic.py!} -``` - -The console output, `stdout` will be: - -```json -{!> ../examples/logging/basic.log!} -``` - -### FastAPI Integration - -You can use the logging system with FastAPI as well: - -```python -{!> ../examples/logging/fastapi.py!} -``` - -!!! warning - It is crucial to call `configure_logging` during the lifespan of the FastAPI application. Failing to do so may result in the FastAPI CLI resetting the logging configuration. diff --git a/.conflict-side-1/docs/sync.md b/.conflict-side-1/docs/sync.md deleted file mode 100644 index 4c3b881..0000000 --- a/.conflict-side-1/docs/sync.md +++ /dev/null @@ -1,81 +0,0 @@ -# Synchronization Primitives - -The `sync` package provides synchronization primitives for distributed systems. - -The primitives are technology agnostic, supporting multiple backends (see more in the Backends section). - -The available primitives are: - -- **[Leader Election](#leader-election)**: A single worker is elected as the leader for performing tasks only once in a cluster. -- **[Lock](#lock)**: A distributed lock that can be used to synchronize access to shared resources. - -The synchronization primitives can be used in combination with the `TaskManager` and `TaskRouter` to control task execution in a distributed system (see more in [Task Scheduler](task.md)). - -## Backend - -You must load a synchronization backend before using synchronization primitives. - -!!! note - Although Grelmicro use AnyIO for concurrency, the backends generally depend on `asyncio`, therefore Trio is not supported. - -You can initialize a backend like this: - -=== "Redis" - ```python - {!> ../examples/sync/redis.py!} - ``` - -=== "Postgres" - ```python - {!> ../examples/sync/postgres.py!} - ``` - -=== "Memory (For Testing Only)" - ```python - {!> ../examples/sync/memory.py!} - ``` - -!!! warning - Please make sure to use a proper way to store connection url, such as environment variables (not like the example above). - -!!! tip - Feel free to create your own backend and contribute it. In the `sync.abc` module, you can find the protocol for creating new backends. - - - -## Leader Election - -Leader election ensures that only one worker in the cluster is designated as the leader at any given time using a distributed lock. - -The leader election service is responsible for acquiring and renewing the distributed lock. It runs as an AnyIO Task that can be easily started with the [Task Manager](./task.md#task-manager). This service operates in the background, automatically renewing the lock to prevent other workers from acquiring it. The lock is released automatically when the task is cancelled or during shutdown. - -=== "Task Manager (Recommended)" - ```python - {!> ../examples/sync/leaderelection_task.py!} - ``` - -=== "AnyIO Task Group (Advanced)" - ```python - {!> ../examples/sync/leaderelection_anyio.py!} - ``` - -## Lock - -The lock is a distributed lock that can be used to synchronize access to shared resources. - -The lock supports the following features: - -- **Async**: The lock must be acquired and released asynchronously. -- **Distributed**: The lock must be distributed across multiple workers. -- **Reentrant**: The lock must allow the same token to acquire it multiple times to extend the lease. -- **Expiring**: The lock must have a timeout to auto-release after an interval to prevent deadlocks. -- **Non-blocking**: Lock operations must not block the async event loop. -- **Vendor-agnostic**: Must support multiple backends (Redis, Postgres, ConfigMap, etc.). - - -```python -{!> ../examples/sync/lock.py!} -``` - -!!! warning - The lock is designed for use within an async event loop and is not thread-safe or process-safe. diff --git a/.conflict-side-1/docs/task.md b/.conflict-side-1/docs/task.md deleted file mode 100644 index b6f0e00..0000000 --- a/.conflict-side-1/docs/task.md +++ /dev/null @@ -1,85 +0,0 @@ -# Task Scheduler - -The `task` package provides a simple task scheduler that can be used to run tasks periodically. - -> **Note**: This is not a replacement for bigger tools like Celery, taskiq, or APScheduler. It is just lightweight, easy to use, and safe for running tasks in a distributed system with synchronization. - -The key features are: - -- **Fast & Easy**: Offers simple decorators to define and schedule tasks effortlessly. -- **Interval Task**: Allows tasks to run at specified intervals. -- **Synchronization**: Controls concurrency using synchronization primitives to manage simultaneous task execution (see the `sync` package). -- **Dependency Injection**: Use [FastDepends](https://lancetnik.github.io/FastDepends/) library to inject dependencies into tasks. -- **Error Handling**: Catches and logs errors, ensuring that task execution failures do not stop the scheduling. - -## Task Manager - -The `TaskManager` class is the main entry point to manage scheduled tasks. You need to start the task manager to run the scheduled tasks using the application lifespan. - -=== "FastAPI" - - ```python - {!> ../examples/task/fastapi.py!} - ``` - -=== "FastStream" - - ```python - - {!> ../examples/task/faststream.py!} - ``` - -## Interval Task - -To create an `IntervalTask`, use the `interval` decorator method of the `TaskManager` instance. This decorator allows tasks to run at specified intervals. - -> **Note**: The interval specifies the waiting time between task executions. Ensure that the task execution duration is considered to meet deadlines effectively. - -=== "TaskManager" - - ```python - {!> ../examples/task/interval_manager.py!} - ``` - -=== "TaskRouter" - - ```python - {!> ../examples/task/interval_router.py!} - ``` - - -## Synchronization - -The Task can be synchronized using a [Synchoronization Primitive](sync.md) to control concurrency and manage simultaneous task execution. - -=== "Lock" - - ```python - {!> ../examples/task/lock.py!} - ``` - - -=== "Leader Election" - - - ```python - {!> ../examples/task/leaderelection.py!} - ``` - -## Task Router - -For bigger applications, you can use the `TaskRouter` class to manage tasks in different modules. - - -```python -{!> ../examples/task/router.py [ln:1-10]!} -``` - -Then you can include the `TaskRouter` into the `TaskManager` or other routers using the `include_router` method. - -```python -{!> ../examples/task/router.py [ln:12-]!} -``` - -!!! tip - The `TaskRouter` follows the same philosophy as the `APIRouter` in FastAPI or the **Router** in FastStream. diff --git a/.conflict-side-1/examples/__init__.py b/.conflict-side-1/examples/__init__.py deleted file mode 100644 index 73b7d32..0000000 --- a/.conflict-side-1/examples/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Examples.""" diff --git a/.conflict-side-1/examples/logging/__init__.py b/.conflict-side-1/examples/logging/__init__.py deleted file mode 100644 index bf04afe..0000000 --- a/.conflict-side-1/examples/logging/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Logging Examples.""" diff --git a/.conflict-side-1/examples/logging/basic.log b/.conflict-side-1/examples/logging/basic.log deleted file mode 100644 index 33c8e37..0000000 --- a/.conflict-side-1/examples/logging/basic.log +++ /dev/null @@ -1,4 +0,0 @@ -{"time":"2024-11-25T15:56:36.066922+01:00","level":"INFO","thread":"MainThread","logger":"__main__::7","msg":"This is an info message"} -{"time":"2024-11-25T15:56:36.067063+01:00","level":"WARNING","thread":"MainThread","logger":"__main__::8","msg":"This is a warning message with context","ctx":{"user":"Alice"}} -{"time":"2024-11-25T15:56:36.067105+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::9","msg":"This is an error message with context","ctx":{"user":"Bob"}} -{"time":"2024-11-25T15:56:36.067134+01:00","level":"ERROR","thread":"MainThread","logger":"__main__::14","msg":"This is an exception message with context","ctx":{"user":"Charlie","exception":"ValueError: This is an exception"}} diff --git a/.conflict-side-1/examples/logging/basic.py b/.conflict-side-1/examples/logging/basic.py deleted file mode 100644 index 889f160..0000000 --- a/.conflict-side-1/examples/logging/basic.py +++ /dev/null @@ -1,17 +0,0 @@ -from loguru import logger - -from grelmicro.logging import configure_logging - -configure_logging() - -logger.debug("This is a debug message") -logger.info("This is an info message") -logger.warning("This is a warning message with context", user="Alice") -logger.error("This is an error message with context", user="Bob") - -try: - raise ValueError("This is an exception message") -except ValueError: - logger.exception( - "This is an exception message with context", user="Charlie" - ) diff --git a/.conflict-side-1/examples/logging/configure_logging.py b/.conflict-side-1/examples/logging/configure_logging.py deleted file mode 100644 index 0ffacd8..0000000 --- a/.conflict-side-1/examples/logging/configure_logging.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.logging import configure_logging - -configure_logging() diff --git a/.conflict-side-1/examples/logging/fastapi.py b/.conflict-side-1/examples/logging/fastapi.py deleted file mode 100644 index 7f318c5..0000000 --- a/.conflict-side-1/examples/logging/fastapi.py +++ /dev/null @@ -1,22 +0,0 @@ -from contextlib import asynccontextmanager - -from fastapi import FastAPI -from loguru import logger - -from grelmicro.logging import configure_logging - - -@asynccontextmanager -def lifespan_startup(): - # Ensure logging is configured during startup - configure_logging() - yield - - -app = FastAPI() - - -@app.get("/") -def root(): - logger.info("This is an info message") - return {"Hello": "World"} diff --git a/.conflict-side-1/examples/simple_fastapi_app.py b/.conflict-side-1/examples/simple_fastapi_app.py deleted file mode 100644 index ff52251..0000000 --- a/.conflict-side-1/examples/simple_fastapi_app.py +++ /dev/null @@ -1,54 +0,0 @@ -from contextlib import asynccontextmanager - -import typer -from fastapi import FastAPI - -from grelmicro.logging.loguru import configure_logging -from grelmicro.sync import LeaderElection, Lock -from grelmicro.sync.redis import RedisSyncBackend -from grelmicro.task import TaskManager - - -# === FastAPI === -@asynccontextmanager -async def lifespan(app): - configure_logging() - # Start the lock backend and task manager - async with sync_backend, task: - yield - - -app = FastAPI(lifespan=lifespan) - - -@app.get("/") -def read_root(): - return {"Hello": "World"} - - -# === Grelmicro === -task = TaskManager() -sync_backend = RedisSyncBackend("redis://localhost:6379/0") - -# --- Ensure that only one say hello world at the same time --- -lock = Lock("say_hello_world") - - -@task.interval(seconds=1, sync=lock) -def say_hello_world_every_second(): - typer.echo("Hello World") - - -@task.interval(seconds=1, sync=lock) -def say_as_well_hello_world_every_second(): - typer.echo("Hello World") - - -# --- Ensure that only one worker is the leader --- -leader_election = LeaderElection("leader-election") -task.add_task(leader_election) - - -@task.interval(seconds=10, sync=leader_election) -def say_hello_leader_every_ten_seconds(): - typer.echo("Hello Leader") diff --git a/.conflict-side-1/examples/single_file_app.py b/.conflict-side-1/examples/single_file_app.py deleted file mode 100644 index 4f4bb87..0000000 --- a/.conflict-side-1/examples/single_file_app.py +++ /dev/null @@ -1,114 +0,0 @@ -import time -from contextlib import asynccontextmanager -from typing import Annotated - -import anyio -import typer -from fast_depends import Depends -from fastapi import FastAPI - -from grelmicro.sync.leaderelection import LeaderElection -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.task import TaskManager - -backend = MemorySyncBackend() -task = TaskManager() - - -@asynccontextmanager -async def lifespan(app): - async with backend, task: - typer.echo("App started") - yield - typer.echo("App stopped") - - -app = FastAPI(lifespan=lifespan) - -leased_lock_10sec = Lock( - name="leased_lock_10sec", - lease_duration=10, - backend=backend, -) -leased_lock_5sec = Lock( - name="leased_lock_5sec", - lease_duration=5, - backend=backend, -) - -leader_election = LeaderElection(name="simple-leader", backend=backend) - -task.add_task(leader_election) - - -@task.interval(seconds=1) -def sync_func_with_no_param(): - typer.echo("sync_with_no_param") - - -@task.interval(seconds=2) -async def async_func_with_no_param(): - typer.echo("async_with_no_param") - - -def sync_dependency(): - return "sync_dependency" - - -@task.interval(seconds=3) -def sync_func_with_sync_dependency( - sync_dependency: Annotated[str, Depends(sync_dependency)], -): - typer.echo(sync_dependency) - - -async def async_dependency(): - yield "async_with_async_dependency" - - -@task.interval(seconds=4) -async def async_func_with_async_dependency( - async_dependency: Annotated[str, Depends(async_dependency)], -): - typer.echo(async_dependency) - - -@task.interval(seconds=15, sync=leased_lock_10sec) -def sync_func_with_leased_lock_10sec(): - typer.echo("sync_func_with_leased_lock_10sec") - time.sleep(9) - - -@task.interval(seconds=15, sync=leased_lock_10sec) -async def async_func_with_leased_lock_10sec(): - typer.echo("async_func_with_leased_lock_10sec") - await anyio.sleep(9) - - -@task.interval(seconds=15, sync=leased_lock_5sec) -def sync_func_with_sync_dependency_and_leased_lock_5sec( - sync_dependency: Annotated[str, Depends(sync_dependency)], -): - typer.echo(sync_dependency) - time.sleep(4) - - -@task.interval(seconds=15, sync=leased_lock_5sec) -async def async_func_with_async_dependency_and_leased_lock_5sec( - async_dependency: Annotated[str, Depends(async_dependency)], -): - typer.echo(async_dependency) - await anyio.sleep(4) - - -@task.interval(seconds=15, sync=leader_election) -def sync_func_with_leader_election(): - typer.echo("sync_func_with_leader_election") - time.sleep(30) - - -@task.interval(seconds=15, sync=leader_election) -async def async_func_with_leader_election(): - typer.echo("async_func_with_leader_election") - await anyio.sleep(30) diff --git a/.conflict-side-1/examples/sync/__init__.py b/.conflict-side-1/examples/sync/__init__.py deleted file mode 100644 index acd409a..0000000 --- a/.conflict-side-1/examples/sync/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Synchronization Primitives Examples.""" diff --git a/.conflict-side-1/examples/sync/leaderelection_anyio.py b/.conflict-side-1/examples/sync/leaderelection_anyio.py deleted file mode 100644 index 784f188..0000000 --- a/.conflict-side-1/examples/sync/leaderelection_anyio.py +++ /dev/null @@ -1,11 +0,0 @@ -from anyio import create_task_group, sleep_forever - -from grelmicro.sync.leaderelection import LeaderElection - -leader = LeaderElection("cluster_group") - - -async def main(): - async with create_task_group() as tg: - await tg.start(leader) - await sleep_forever() diff --git a/.conflict-side-1/examples/sync/leaderelection_task.py b/.conflict-side-1/examples/sync/leaderelection_task.py deleted file mode 100644 index 58fa926..0000000 --- a/.conflict-side-1/examples/sync/leaderelection_task.py +++ /dev/null @@ -1,6 +0,0 @@ -from grelmicro.sync import LeaderElection -from grelmicro.task import TaskManager - -leader = LeaderElection("cluster_group") -task = TaskManager() -task.add_task(leader) diff --git a/.conflict-side-1/examples/sync/lock.py b/.conflict-side-1/examples/sync/lock.py deleted file mode 100644 index 7f38fe6..0000000 --- a/.conflict-side-1/examples/sync/lock.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.sync import Lock - -lock = Lock("resource_name") - - -async def main(): - async with lock: - print("Protected resource accessed") diff --git a/.conflict-side-1/examples/sync/memory.py b/.conflict-side-1/examples/sync/memory.py deleted file mode 100644 index 7eefea9..0000000 --- a/.conflict-side-1/examples/sync/memory.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.memory import MemorySyncBackend - -backend = MemorySyncBackend() diff --git a/.conflict-side-1/examples/sync/postgres.py b/.conflict-side-1/examples/sync/postgres.py deleted file mode 100644 index ea8b8c3..0000000 --- a/.conflict-side-1/examples/sync/postgres.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.postgres import PostgresSyncBackend - -backend = PostgresSyncBackend("postgresql://user:password@localhost:5432/db") diff --git a/.conflict-side-1/examples/sync/redis.py b/.conflict-side-1/examples/sync/redis.py deleted file mode 100644 index 0625f5d..0000000 --- a/.conflict-side-1/examples/sync/redis.py +++ /dev/null @@ -1,3 +0,0 @@ -from grelmicro.sync.redis import RedisSyncBackend - -backend = RedisSyncBackend("redis://localhost:6379/0") diff --git a/.conflict-side-1/examples/task/__init__.py b/.conflict-side-1/examples/task/__init__.py deleted file mode 100644 index 20f7752..0000000 --- a/.conflict-side-1/examples/task/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Task Scheduler Examples.""" diff --git a/.conflict-side-1/examples/task/fastapi.py b/.conflict-side-1/examples/task/fastapi.py deleted file mode 100644 index 16aaa8e..0000000 --- a/.conflict-side-1/examples/task/fastapi.py +++ /dev/null @@ -1,16 +0,0 @@ -from contextlib import asynccontextmanager - -from fastapi import FastAPI - -from grelmicro.task import TaskManager - -task = TaskManager() - - -@asynccontextmanager -async def lifespan(app: FastAPI): - async with task: - yield - - -app = FastAPI(lifespan=lifespan) diff --git a/.conflict-side-1/examples/task/faststream.py b/.conflict-side-1/examples/task/faststream.py deleted file mode 100644 index 688c8d9..0000000 --- a/.conflict-side-1/examples/task/faststream.py +++ /dev/null @@ -1,18 +0,0 @@ -from contextlib import asynccontextmanager - -from faststream import ContextRepo, FastStream -from faststream.redis import RedisBroker - -from grelmicro.task import TaskManager - -task = TaskManager() - - -@asynccontextmanager -async def lifespan(context: ContextRepo): - async with task: - yield - - -broker = RedisBroker() -app = FastStream(broker, lifespan=lifespan) diff --git a/.conflict-side-1/examples/task/interval_manager.py b/.conflict-side-1/examples/task/interval_manager.py deleted file mode 100644 index 91beb2e..0000000 --- a/.conflict-side-1/examples/task/interval_manager.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.task import TaskManager - -task = TaskManager() - - -@task.interval(seconds=5) -async def my_task(): - print("Hello, World!") diff --git a/.conflict-side-1/examples/task/interval_router.py b/.conflict-side-1/examples/task/interval_router.py deleted file mode 100644 index f114ad7..0000000 --- a/.conflict-side-1/examples/task/interval_router.py +++ /dev/null @@ -1,8 +0,0 @@ -from grelmicro.task import TaskRouter - -task = TaskRouter() - - -@task.interval(seconds=5) -async def my_task(): - print("Hello, World!") diff --git a/.conflict-side-1/examples/task/leaderelection.py b/.conflict-side-1/examples/task/leaderelection.py deleted file mode 100644 index ad12773..0000000 --- a/.conflict-side-1/examples/task/leaderelection.py +++ /dev/null @@ -1,12 +0,0 @@ -from grelmicro.sync import LeaderElection -from grelmicro.task import TaskManager - -leader = LeaderElection("my_task") -task = TaskManager() -task.add_task(leader) - - -@task.interval(seconds=5, sync=leader) -async def my_task(): - async with leader: - print("Hello, World!") diff --git a/.conflict-side-1/examples/task/lock.py b/.conflict-side-1/examples/task/lock.py deleted file mode 100644 index cdbf795..0000000 --- a/.conflict-side-1/examples/task/lock.py +++ /dev/null @@ -1,11 +0,0 @@ -from grelmicro.sync import Lock -from grelmicro.task import TaskManager - -lock = Lock("my_task") -task = TaskManager() - - -@task.interval(seconds=5, sync=lock) -async def my_task(): - async with lock: - print("Hello, World!") diff --git a/.conflict-side-1/examples/task/router.py b/.conflict-side-1/examples/task/router.py deleted file mode 100644 index 2b166aa..0000000 --- a/.conflict-side-1/examples/task/router.py +++ /dev/null @@ -1,15 +0,0 @@ -from grelmicro.task import TaskRouter - - -router = TaskRouter() - - -@router.interval(seconds=5) -async def my_task(): - print("Hello, World!") - - -from grelmicro.task.manager import TaskManager - -task = TaskManager() -task.include_router(router) diff --git a/.conflict-side-1/grelmicro/__init__.py b/.conflict-side-1/grelmicro/__init__.py deleted file mode 100644 index 7cc6d82..0000000 --- a/.conflict-side-1/grelmicro/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python.""" # noqa: E501 - -__version__ = "0.2.2" diff --git a/.conflict-side-1/grelmicro/errors.py b/.conflict-side-1/grelmicro/errors.py deleted file mode 100644 index 141f82e..0000000 --- a/.conflict-side-1/grelmicro/errors.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Grelmicro Errors.""" - -from typing import assert_never - -from pydantic import ValidationError - - -class GrelmicroError(Exception): - """Base Grelmicro error.""" - - -class OutOfContextError(GrelmicroError, RuntimeError): - """Outside Context Error. - - Raised when a method is called outside of the context manager. - """ - - def __init__(self, cls: object, method_name: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not call {cls.__class__.__name__}.{method_name} outside of the context manager" - ) - - -class DependencyNotFoundError(GrelmicroError, ImportError): - """Dependency Not Found Error.""" - - def __init__(self, *, module: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not import module {module}, try running 'pip install {module}'" - ) - - -class SettingsValidationError(GrelmicroError, ValueError): - """Settings Validation Error.""" - - def __init__(self, error: ValidationError | str) -> None: - """Initialize the error.""" - if isinstance(error, str): - details = error - elif isinstance(error, ValidationError): - details = "\n".join( - f"- {data['loc'][0]}: {data['msg']} [input={data['input']}]" - for data in error.errors() - ) - else: - assert_never(error) - - super().__init__( - f"Could not validate environment variables settings:\n{details}" - ) diff --git a/.conflict-side-1/grelmicro/logging/__init__.py b/.conflict-side-1/grelmicro/logging/__init__.py deleted file mode 100644 index 60d3d45..0000000 --- a/.conflict-side-1/grelmicro/logging/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Grelmicro Logging.""" - -from grelmicro.logging.loguru import configure_logging - -__all__ = ["configure_logging"] diff --git a/.conflict-side-1/grelmicro/logging/config.py b/.conflict-side-1/grelmicro/logging/config.py deleted file mode 100644 index a6301c1..0000000 --- a/.conflict-side-1/grelmicro/logging/config.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Logging Configuration.""" - -from enum import StrEnum -from typing import Self - -from pydantic import Field -from pydantic_settings import BaseSettings - - -class _CaseInsensitiveEnum(StrEnum): - @classmethod - def _missing_(cls, value: object) -> Self | None: - value = str(value).lower() - for member in cls: - if member.lower() == value: - return member - return None - - -class LoggingLevelType(_CaseInsensitiveEnum): - """Logging Level Enum.""" - - DEBUG = "DEBUG" - INFO = "INFO" - WARNING = "WARNING" - ERROR = "ERROR" - CRITICAL = "CRITICAL" - - -class LoggingFormatType(_CaseInsensitiveEnum): - """Logging Format Enum.""" - - JSON = "JSON" - TEXT = "TEXT" - - -class LoggingSettings(BaseSettings): - """Logging Settings.""" - - LOG_LEVEL: LoggingLevelType = LoggingLevelType.INFO - LOG_FORMAT: LoggingFormatType | str = Field( - LoggingFormatType.JSON, union_mode="left_to_right" - ) diff --git a/.conflict-side-1/grelmicro/logging/errors.py b/.conflict-side-1/grelmicro/logging/errors.py deleted file mode 100644 index 097006f..0000000 --- a/.conflict-side-1/grelmicro/logging/errors.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Grelmicro Logging Errors.""" - -from grelmicro.errors import SettingsValidationError - - -class LoggingSettingsValidationError(SettingsValidationError): - """Logging Settings Validation Error.""" diff --git a/.conflict-side-1/grelmicro/logging/loguru.py b/.conflict-side-1/grelmicro/logging/loguru.py deleted file mode 100644 index a94202c..0000000 --- a/.conflict-side-1/grelmicro/logging/loguru.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Loguru Logging.""" - -import json -import sys -from collections.abc import Mapping -from typing import TYPE_CHECKING, Any, NotRequired - -from pydantic import ValidationError -from typing_extensions import TypedDict - -from grelmicro.errors import DependencyNotFoundError -from grelmicro.logging.config import LoggingFormatType, LoggingSettings -from grelmicro.logging.errors import LoggingSettingsValidationError - -if TYPE_CHECKING: - from loguru import FormatFunction, Record - -try: - import loguru -except ImportError: # pragma: no cover - loguru = None # type: ignore[assignment] - -try: - import orjson - - def _json_dumps(obj: Mapping[str, Any]) -> str: - return orjson.dumps(obj).decode("utf-8") -except ImportError: # pragma: no cover - import json - - _json_dumps = json.dumps - - -JSON_FORMAT = "{extra[serialized]}" -TEXT_FORMAT = ( - "{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | " - "{name}:{function}:{line} - {message}" -) - - -class JSONRecordDict(TypedDict): - """JSON log record representation. - - The time use a ISO 8601 string. - """ - - time: str - level: str - msg: str - logger: str | None - thread: str - ctx: NotRequired[dict[Any, Any]] - - -def json_patcher(record: "Record") -> None: - """Patch the serialized log record with `JSONRecordDict` representation.""" - json_record = JSONRecordDict( - time=record["time"].isoformat(), - level=record["level"].name, - thread=record["thread"].name, - logger=f'{record["name"]}:{record["function"]}:{record["line"]}', - msg=record["message"], - ) - - ctx = {k: v for k, v in record["extra"].items() if k != "serialized"} - exception = record["exception"] - - if exception and exception.type: - ctx["exception"] = f"{exception.type.__name__}: {exception.value!s}" - - if ctx: - json_record["ctx"] = ctx - - record["extra"]["serialized"] = _json_dumps(json_record) - - -def json_formatter(record: "Record") -> str: - """Format log record with `JSONRecordDict` representation. - - This function does not return the formatted record directly but provides the format to use when - writing to the sink. - """ - json_patcher(record) - return JSON_FORMAT + "\n" - - -def configure_logging() -> None: - """Configure logging with loguru. - - Simple twelve-factor app logging configuration that logs to stdout. - - The following environment variables are used: - - LOG_LEVEL: The log level to use (default: INFO). - - LOG_FORMAT: JSON | TEXT or any loguru template to format logged message (default: JSON). - - Raises: - MissingDependencyError: If the loguru module is not installed. - LoggingSettingsError: If the LOG_FORMAT or LOG_LEVEL environment variable is invalid - """ - if not loguru: - raise DependencyNotFoundError(module="loguru") - - try: - settings = LoggingSettings() - except ValidationError as error: - raise LoggingSettingsValidationError(error) from None - - logger = loguru.logger - log_format: str | FormatFunction = settings.LOG_FORMAT - - if log_format is LoggingFormatType.JSON: - log_format = json_formatter - elif log_format is LoggingFormatType.TEXT: - log_format = TEXT_FORMAT - - logger.remove() - logger.add( - sys.stdout, - level=settings.LOG_LEVEL, - format=log_format, - ) diff --git a/.conflict-side-1/grelmicro/py.typed b/.conflict-side-1/grelmicro/py.typed deleted file mode 100644 index e69de29..0000000 diff --git a/.conflict-side-1/grelmicro/sync/__init__.py b/.conflict-side-1/grelmicro/sync/__init__.py deleted file mode 100644 index 128d56c..0000000 --- a/.conflict-side-1/grelmicro/sync/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Grelmicro Synchronization Primitives.""" - -from grelmicro.sync.leaderelection import LeaderElection -from grelmicro.sync.lock import Lock - -__all__ = ["LeaderElection", "Lock"] diff --git a/.conflict-side-1/grelmicro/sync/_backends.py b/.conflict-side-1/grelmicro/sync/_backends.py deleted file mode 100644 index 66f4b9f..0000000 --- a/.conflict-side-1/grelmicro/sync/_backends.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Grelmicro Backend Registry. - -Contains loaded backends of each type to be used as default. - -Note: - For now, only lock backends are supported, but other backends may be added in the future. -""" - -from typing import Literal, NotRequired, TypedDict - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import BackendNotLoadedError - - -class LoadedBackendsDict(TypedDict): - """Loaded backends type.""" - - lock: NotRequired[SyncBackend] - - -loaded_backends: LoadedBackendsDict = {} - - -def get_sync_backend() -> SyncBackend: - """Get the lock backend.""" - backend: Literal["lock"] = "lock" - try: - return loaded_backends[backend] - except KeyError: - raise BackendNotLoadedError(backend) from None diff --git a/.conflict-side-1/grelmicro/sync/_base.py b/.conflict-side-1/grelmicro/sync/_base.py deleted file mode 100644 index a0e6fb0..0000000 --- a/.conflict-side-1/grelmicro/sync/_base.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Grelmicro Lock API.""" - -from types import TracebackType -from typing import Annotated, Protocol, Self -from uuid import UUID - -from pydantic import BaseModel, ConfigDict -from typing_extensions import Doc - -from grelmicro.sync.abc import Synchronization - - -class BaseLockConfig(BaseModel): - """Base Lock Config.""" - - model_config = ConfigDict(frozen=True, extra="forbid") - - name: Annotated[ - str, - Doc(""" - The name of the resource to lock. - """), - ] - worker: Annotated[ - str | UUID, - Doc(""" - The worker identity. - - By default, use a UUIDv1. - """), - ] - - -class BaseLock(Synchronization, Protocol): - """Base Lock Protocol.""" - - async def __aenter__(self) -> Self: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - """ - ... - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - ... - - @property - def config(self) -> BaseLockConfig: - """Return the config.""" - ... - - async def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - ... - - async def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - WouldBlock: If the lock cannot be acquired without blocking. - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - ... - - async def release(self) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - ... - - async def locked(self) -> bool: - """Check if the lock is currently held.""" - ... - - async def owned(self) -> bool: - """Check if the lock is currently held by the current token.""" - ... diff --git a/.conflict-side-1/grelmicro/sync/_utils.py b/.conflict-side-1/grelmicro/sync/_utils.py deleted file mode 100644 index 2ad5dda..0000000 --- a/.conflict-side-1/grelmicro/sync/_utils.py +++ /dev/null @@ -1,38 +0,0 @@ -from threading import get_ident -from uuid import NAMESPACE_DNS, UUID, uuid3 - -from anyio import get_current_task - - -def generate_worker_namespace(worker: str) -> UUID: - """Generate a worker UUIDv3 namespace. - - Generate a worker UUID using UUIDv3 with the DNS namespace. - """ - return uuid3(namespace=NAMESPACE_DNS, name=worker) - - -def generate_task_token(worker: UUID | str) -> str: - """Generate a task UUID. - - The worker namespace is generated using `generate_worker_uuid` if the worker is a string. - Generate a task UUID using UUIDv3 with the worker namespace and the async task ID. - """ - worker = ( - generate_worker_namespace(worker) if isinstance(worker, str) else worker - ) - task = str(get_current_task().id) - return str(uuid3(namespace=worker, name=task)) - - -def generate_thread_token(worker: UUID | str) -> str: - """Generate a thread UUID. - - The worker namespace is generated using `generate_worker_uuid` if the worker is a string. - Generate a thread UUID using UUIDv3 with the worker namespace and the current thread ID. - """ - worker = ( - generate_worker_namespace(worker) if isinstance(worker, str) else worker - ) - thread = str(get_ident()) - return str(uuid3(namespace=worker, name=thread)) diff --git a/.conflict-side-1/grelmicro/sync/abc.py b/.conflict-side-1/grelmicro/sync/abc.py deleted file mode 100644 index 507477c..0000000 --- a/.conflict-side-1/grelmicro/sync/abc.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Grelmicro Synchronization Abstract Base Classes and Protocols.""" - -from types import TracebackType -from typing import Protocol, Self, runtime_checkable - -from pydantic import PositiveFloat - - -class SyncBackend(Protocol): - """Synchronization Backend Protocol. - - This is the low level API for the distributed lock backend that is platform agnostic. - """ - - async def __aenter__(self) -> Self: - """Open the lock backend.""" - ... - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Close the lock backend.""" - ... - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock. - - Args: - name: The name of the lock. - token: The token to acquire the lock. - duration: The duration in seconds to hold the lock. - - Returns: - True if the lock is acquired, False if the lock is already acquired by another token. - - Raises: - Exception: Any exception can be raised if the lock cannot be acquired. - """ - ... - - async def release(self, *, name: str, token: str) -> bool: - """Release a lock. - - Args: - name: The name of the lock. - token: The token to release the lock. - - Returns: - True if the lock was released, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock cannot be released. - """ - ... - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired. - - Args: - name: The name of the lock. - - Returns: - True if the lock is acquired, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock status cannot be checked. - """ - ... - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned. - - Args: - name: The name of the lock. - token: The token to check. - - Returns: - True if the lock is owned by the token, False otherwise. - - Raises: - Exception: Any exception can be raised if the lock status cannot be checked. - """ - ... - - -@runtime_checkable -class Synchronization(Protocol): - """Synchronization Primitive Protocol.""" - - async def __aenter__(self) -> Self: - """Enter the synchronization primitive.""" - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the synchronization primitive.""" - ... - - -Seconds = PositiveFloat diff --git a/.conflict-side-1/grelmicro/sync/errors.py b/.conflict-side-1/grelmicro/sync/errors.py deleted file mode 100644 index 6384e36..0000000 --- a/.conflict-side-1/grelmicro/sync/errors.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Grelmicro Synchronization Primitive Errors.""" - -from grelmicro.errors import SettingsValidationError - - -class SyncError(Exception): - """Synchronization Primitive Error. - - This the base class for all lock errors. - """ - - -class SyncBackendError(SyncError): - """Synchronization Backend Error.""" - - -class BackendNotLoadedError(SyncBackendError): - """Backend Not Loaded Error.""" - - def __init__(self, backend_name: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not load backend {backend_name}, try initializing one first" - ) - - -class LockAcquireError(SyncBackendError): - """Acquire Lock Error. - - This error is raised when an error on backend side occurs during lock acquisition. - """ - - def __init__(self, *, name: str, token: str) -> None: - """Initialize the error.""" - super().__init__(f"Failed to acquire lock: name={name}, token={token}") - - -class LockReleaseError(SyncBackendError): - """Lock Release Error. - - This error is raised when an error on backend side occurs during lock release. - """ - - def __init__( - self, *, name: str, token: str, reason: str | None = None - ) -> None: - """Initialize the error.""" - super().__init__( - f"Failed to release lock: name={name}, token={token}" - + (f", reason={reason}" if reason else ""), - ) - - -class LockNotOwnedError(LockReleaseError): - """Lock Not Owned Error during Release. - - This error is raised when an attempt is made to release a lock that is not owned, respectively - the token is different or the lock is already expired. - """ - - def __init__(self, *, name: str, token: str) -> None: - """Initialize the error.""" - super().__init__(name=name, token=token, reason="lock not owned") - - -class SyncSettingsValidationError(SyncError, SettingsValidationError): - """Synchronization Settings Validation Error.""" diff --git a/.conflict-side-1/grelmicro/sync/leaderelection.py b/.conflict-side-1/grelmicro/sync/leaderelection.py deleted file mode 100644 index 62ce539..0000000 --- a/.conflict-side-1/grelmicro/sync/leaderelection.py +++ /dev/null @@ -1,386 +0,0 @@ -"""Leader Election.""" - -from logging import getLogger -from time import monotonic -from types import TracebackType -from typing import TYPE_CHECKING, Annotated, Self -from uuid import UUID, uuid1 - -from anyio import ( - TASK_STATUS_IGNORED, - CancelScope, - Condition, - fail_after, - get_cancelled_exc_class, - move_on_after, - sleep, -) -from anyio.abc import TaskStatus -from pydantic import BaseModel, model_validator -from typing_extensions import Doc - -from grelmicro.sync._backends import get_sync_backend -from grelmicro.sync.abc import Seconds, SyncBackend, Synchronization -from grelmicro.task.abc import Task - -if TYPE_CHECKING: - from contextlib import AsyncExitStack - - from anyio.abc import TaskGroup - -logger = getLogger("grelmicro.leader_election") - - -class LeaderElectionConfig(BaseModel): - """Leader Election Config. - - Leader election based on a leased reentrant distributed lock. - """ - - name: Annotated[ - str, - Doc( - """ - The leader election lock name. - """, - ), - ] - worker: Annotated[ - str | UUID, - Doc( - """ - The worker identity used as lock token. - """, - ), - ] - lease_duration: Annotated[ - Seconds, - Doc( - """ - The lease duration in seconds. - """, - ), - ] = 15 - renew_deadline: Annotated[ - Seconds, - Doc( - """ - The renew deadline in seconds. - """, - ), - ] = 10 - retry_interval: Annotated[ - Seconds, - Doc( - """ - The retry interval in seconds. - """, - ), - ] = 2 - backend_timeout: Annotated[ - Seconds, - Doc( - """ - The backend timeout in seconds. - """, - ), - ] = 5 - error_interval: Annotated[ - Seconds, - Doc( - """ - The error interval in seconds. - """, - ), - ] = 30 - - @model_validator(mode="after") - def _validate(self) -> Self: - if self.renew_deadline >= self.lease_duration: - msg = "Renew deadline must be shorter than lease duration" - raise ValueError(msg) - if self.retry_interval >= self.renew_deadline: - msg = "Retry interval must be shorter than renew deadline" - raise ValueError(msg) - if self.backend_timeout >= self.renew_deadline: - msg = "Backend timeout must be shorter than renew deadline" - raise ValueError(msg) - return self - - -class LeaderElection(Synchronization, Task): - """Leader Election. - - The leader election is a synchronization primitive with the worker as scope. - It runs as a task to acquire or renew the distributed lock. - """ - - def __init__( - self, - name: Annotated[ - str, - Doc( - """ - The name of the resource representing the leader election. - - It will be used as the lock name so make sure it is unique on the distributed lock - backend. - """, - ), - ], - *, - backend: Annotated[ - SyncBackend | None, - Doc( - """ - The distributed lock backend used to acquire and release the lock. - - By default, it will use the lock backend registry to get the default lock backend. - """, - ), - ] = None, - worker: Annotated[ - str | UUID | None, - Doc( - """ - The worker identity. - - By default, use a UUIDv1 will be generated. - """, - ), - ] = None, - lease_duration: Annotated[ - Seconds, - Doc( - """ - The duration in seconds after the lock will be released if not renewed. - - If the worker becomes unavailable, the lock can only be acquired by an other worker - after it' has expired. - """, - ), - ] = 15, - renew_deadline: Annotated[ - Seconds, - Doc( - """ - The duration in seconds that the leader worker will try to acquire the lock before - giving up. - - Must be shorter than the lease duration. In case of multiple failures, the leader - worker will loose the lead to prevent split-brain scenarios and ensure that only one - worker is the leader at any time. - """, - ), - ] = 10, - retry_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between attempts to acquire or renew the lock. - - Must be shorter than the renew deadline. A shorter schedule enables faster leader - elections but may increase load on the distributed lock backend, while a longer - schedule reduces load but can delay new leader elections. - """, - ), - ] = 2, - backend_timeout: Annotated[ - Seconds, - Doc( - """ - The duration in seconds for waiting on backend for acquiring and releasing the lock. - - This value determines how long the system will wait before giving up the current - operation. - """, - ), - ] = 5, - error_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between logging error messages. - - If shorter than the retry interval, it will log every error. It is used to prevent - flooding the logs when the lock backend is unavailable. - """, - ), - ] = 30, - ) -> None: - """Initialize the leader election.""" - self.config = LeaderElectionConfig( - name=name, - worker=worker or uuid1(), - lease_duration=lease_duration, - renew_deadline=renew_deadline, - retry_interval=retry_interval, - backend_timeout=backend_timeout, - error_interval=error_interval, - ) - self.backend = backend or get_sync_backend() - - self._service_running = False - self._state_change_condition: Condition = Condition() - self._is_leader: bool = False - self._state_updated_at: float = monotonic() - self._error_logged_at: float | None = None - self._task_group: TaskGroup | None = None - self._exit_stack: AsyncExitStack | None = None - - async def __aenter__(self) -> Self: - """Wait for the leader with the context manager.""" - await self.wait_for_leader() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the context manager.""" - - @property - def name(self) -> str: - """Return the task name.""" - return self.config.name - - def is_running(self) -> bool: - """Check if the leader election task is running.""" - return self._service_running - - def is_leader(self) -> bool: - """Check if the current worker is the leader. - - To avoid a split-brain scenario, the leader considers itself as no longer leader if the - renew deadline is reached. - - Returns: - True if the current worker is the leader, False otherwise. - - """ - if not self._is_leader: - return False - return not self._is_renew_deadline_reached() - - async def wait_for_leader(self) -> None: - """Wait until the current worker is the leader.""" - while not self.is_leader(): - async with self._state_change_condition: - await self._state_change_condition.wait() - - async def wait_lose_leader(self) -> None: - """Wait until the current worker is no longer the leader.""" - while self.is_leader(): - with move_on_after(self._seconds_before_expiration_deadline()): - async with self._state_change_condition: - await self._state_change_condition.wait() - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run polling loop service to acquire or renew the distributed lock.""" - task_status.started() - if self._service_running: - logger.warning("Leader Election already running: %s", self.name) - return - self._service_running = True - logger.info("Leader Election started: %s", self.name) - try: - while True: - await self._try_acquire_or_renew() - await sleep(self.config.retry_interval) - except get_cancelled_exc_class(): - logger.info("Leader Election stopped: %s", self.name) - raise - except BaseException: - logger.exception("Leader Election crashed: %s", self.name) - raise - finally: - self._service_running = False - with CancelScope(shield=True): - await self._release() - - async def _update_state( - self, *, is_leader: bool, raison_if_no_more_leader: str - ) -> None: - """Update the state of the leader election.""" - self._state_updated_at = monotonic() - if is_leader is self._is_leader: - return # No change - - self._is_leader = is_leader - - if is_leader: - logger.info("Leader Election acquired leadership: %s", self.name) - else: - logger.warning( - "Leader Election lost leadership: %s (%s)", - self.name, - raison_if_no_more_leader, - ) - - async with self._state_change_condition: - self._state_change_condition.notify_all() - - async def _try_acquire_or_renew(self) -> None: - """Try to acquire leadership.""" - try: - with fail_after(self.config.backend_timeout): - is_leader = await self.backend.acquire( - name=self.name, - token=str(self.config.worker), - duration=self.config.lease_duration, - ) - except Exception: - if self._check_error_interval(): - logger.exception( - "Leader Election failed to acquire lock: %s", self.name - ) - if self._is_renew_deadline_reached(): - await self._update_state( - is_leader=False, - raison_if_no_more_leader="renew deadline reached", - ) - else: - await self._update_state( - is_leader=is_leader, - raison_if_no_more_leader="lock not acquired", - ) - - def _seconds_before_expiration_deadline(self) -> float: - return max( - self._state_updated_at + self.config.lease_duration - monotonic(), 0 - ) - - def _check_error_interval(self) -> bool: - """Check if the cooldown interval allows to log the error.""" - is_logging_allowed = ( - not self._error_logged_at - or (monotonic() - self._error_logged_at) - > self.config.error_interval - ) - self._error_logged_at = monotonic() - return is_logging_allowed - - def _is_renew_deadline_reached(self) -> bool: - return ( - monotonic() - self._state_updated_at - ) >= self.config.renew_deadline - - async def _release(self) -> None: - try: - with fail_after(self.config.backend_timeout): - if not ( - await self.backend.release( - name=self.config.name, token=str(self.config.worker) - ) - ): - logger.info( - "Leader Election lock already released: %s", self.name - ) - except Exception: - logger.exception( - "Leader Election failed to release lock: %s", self.name - ) diff --git a/.conflict-side-1/grelmicro/sync/lock.py b/.conflict-side-1/grelmicro/sync/lock.py deleted file mode 100644 index c87d08f..0000000 --- a/.conflict-side-1/grelmicro/sync/lock.py +++ /dev/null @@ -1,324 +0,0 @@ -"""Grelmicro Lock.""" - -from time import sleep as thread_sleep -from types import TracebackType -from typing import Annotated, Self -from uuid import UUID, uuid1 - -from anyio import WouldBlock, from_thread, sleep -from typing_extensions import Doc - -from grelmicro.sync._backends import get_sync_backend -from grelmicro.sync._base import BaseLock, BaseLockConfig -from grelmicro.sync._utils import generate_task_token, generate_thread_token -from grelmicro.sync.abc import Seconds, SyncBackend -from grelmicro.sync.errors import ( - LockAcquireError, - LockNotOwnedError, - LockReleaseError, - SyncBackendError, -) - - -class LockConfig(BaseLockConfig, frozen=True, extra="forbid"): - """Lock Config.""" - - lease_duration: Annotated[ - Seconds, - Doc( - """ - The lease duration in seconds for the lock. - """, - ), - ] - retry_interval: Annotated[ - Seconds, - Doc( - """ - The interval in seconds between attempts to acquire the lock. - """, - ), - ] - - -class Lock(BaseLock): - """Lock. - - This lock is a distributed lock that is used to acquire a resource across multiple workers. The - lock is acquired asynchronously and can be extended multiple times manually. The lock is - automatically released after a duration if not extended. - """ - - def __init__( - self, - name: Annotated[ - str, - Doc( - """ - The name of the resource to lock. - - It will be used as the lock name so make sure it is unique on the lock backend. - """, - ), - ], - *, - backend: Annotated[ - SyncBackend | None, - Doc(""" - The distributed lock backend used to acquire and release the lock. - - By default, it will use the lock backend registry to get the default lock backend. - """), - ] = None, - worker: Annotated[ - str | UUID | None, - Doc( - """ - The worker identity. - - By default, use a UUIDv1 will be generated. - """, - ), - ] = None, - lease_duration: Annotated[ - Seconds, - Doc( - """ - The duration in seconds for the lock to be held by default. - """, - ), - ] = 60, - retry_interval: Annotated[ - Seconds, - Doc( - """ - The duration in seconds between attempts to acquire the lock. - - Should be greater or equal than 0.1 to prevent flooding the lock backend. - """, - ), - ] = 0.1, - ) -> None: - """Initialize the lock.""" - self._config: LockConfig = LockConfig( - name=name, - worker=worker or uuid1(), - lease_duration=lease_duration, - retry_interval=retry_interval, - ) - self.backend = backend or get_sync_backend() - self._from_thread: ThreadLockAdapter | None = None - - async def __aenter__(self) -> Self: - """Acquire the lock with the async context manager.""" - await self.acquire() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock with the async context manager. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - await self.release() - - @property - def config(self) -> LockConfig: - """Return the lock config.""" - return self._config - - @property - def from_thread(self) -> "ThreadLockAdapter": - """Return the lock adapter for worker thread.""" - if self._from_thread is None: - self._from_thread = ThreadLockAdapter(lock=self) - return self._from_thread - - async def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - while not await self.do_acquire(token=token): # noqa: ASYNC110 // Polling is intentional - await sleep(self._config.retry_interval) - - async def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - WouldBlock: If the lock cannot be acquired without blocking. - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - if not await self.do_acquire(token=token): - msg = f"Lock not acquired: name={self._config.name}, token={token}" - raise WouldBlock(msg) - - async def release(self) -> None: - """Release the lock. - - Raises: - LockNotOwnedError: If the lock is not owned by the current token. - LockReleaseError: If the lock cannot be released due to an error on the backend. - - """ - token = generate_task_token(self._config.worker) - if not await self.do_release(token): - raise LockNotOwnedError(name=self._config.name, token=token) - - async def locked(self) -> bool: - """Check if the lock is acquired. - - Raise: - SyncBackendError: If the lock cannot be checked due to an error on the backend. - """ - try: - return await self.backend.locked(name=self._config.name) - except Exception as exc: - msg = "Failed to check if the lock is acquired" - raise SyncBackendError(msg) from exc - - async def owned(self) -> bool: - """Check if the lock is owned by the current token. - - Raise: - SyncBackendError: If the lock cannot be checked due to an error on the backend. - """ - return await self.do_owned(generate_task_token(self._config.worker)) - - async def do_acquire(self, token: str) -> bool: - """Acquire the lock. - - This method should not be called directly. Use `acquire` instead. - - Returns: - bool: True if the lock was acquired, False if the lock was not acquired. - - Raises: - LockAcquireError: If the lock cannot be acquired due to an error on the backend. - """ - try: - return await self.backend.acquire( - name=self._config.name, - token=token, - duration=self._config.lease_duration, - ) - except Exception as exc: - raise LockAcquireError(name=self._config.name, token=token) from exc - - async def do_release(self, token: str) -> bool: - """Release the lock. - - This method should not be called directly. Use `release` instead. - - Returns: - bool: True if the lock was released, False otherwise. - - Raises: - LockReleaseError: Cannot release the lock due to backend error. - """ - try: - return await self.backend.release( - name=self._config.name, token=token - ) - except Exception as exc: - raise LockReleaseError(name=self._config.name, token=token) from exc - - async def do_owned(self, token: str) -> bool: - """Check if the lock is owned by the current token. - - This method should not be called directly. Use `owned` instead. - - Returns: - bool: True if the lock is owned by the current token, False otherwise. - - Raises: - SyncBackendError: Cannot check if the lock is owned due to backend error. - """ - try: - return await self.backend.owned(name=self._config.name, token=token) - except Exception as exc: - msg = "Failed to check if the lock is owned" - raise SyncBackendError(msg) from exc - - -class ThreadLockAdapter: - """Lock Adapter for Worker Thread.""" - - def __init__(self, lock: Lock) -> None: - """Initialize the lock adapter.""" - self._lock = lock - - def __enter__(self) -> Self: - """Acquire the lock with the context manager.""" - self.acquire() - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Release the lock with the context manager.""" - self.release() - - def acquire(self) -> None: - """Acquire the lock. - - Raises: - LockAcquireError: Cannot acquire the lock due to backend error. - - """ - token = generate_thread_token(self._lock.config.worker) - retry_interval = self._lock.config.retry_interval - while not from_thread.run(self._lock.do_acquire, token): - thread_sleep(retry_interval) - - def acquire_nowait(self) -> None: - """ - Acquire the lock, without blocking. - - Raises: - LockAcquireError: Cannot acquire the lock due to backend error. - WouldBlock: If the lock cannot be acquired without blocking. - - """ - token = generate_thread_token(self._lock.config.worker) - if not from_thread.run(self._lock.do_acquire, token): - msg = f"Lock not acquired: name={self._lock.config.name}, token={token}" - raise WouldBlock(msg) - - def release(self) -> None: - """Release the lock. - - Raises: - ReleaseSyncBackendError: Cannot release the lock due to backend error. - LockNotOwnedError: If the lock is not currently held. - - """ - token = generate_thread_token(self._lock.config.worker) - if not from_thread.run(self._lock.do_release, token): - raise LockNotOwnedError(name=self._lock.config.name, token=token) - - def locked(self) -> bool: - """Return True if the lock is currently held.""" - return from_thread.run(self._lock.locked) - - def owned(self) -> bool: - """Return True if the lock is currently held by the current worker thread.""" - return from_thread.run( - self._lock.do_owned, generate_thread_token(self._lock.config.worker) - ) diff --git a/.conflict-side-1/grelmicro/sync/memory.py b/.conflict-side-1/grelmicro/sync/memory.py deleted file mode 100644 index 9746c59..0000000 --- a/.conflict-side-1/grelmicro/sync/memory.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Memory Synchronization Backend.""" - -from time import monotonic -from types import TracebackType -from typing import Annotated, Self - -from typing_extensions import Doc - -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend - - -class MemorySyncBackend(SyncBackend): - """Memory Synchronization Backend. - - This is not a backend with a real distributed lock. It is a local lock that can be used for - testing purposes or for locking operations that are executed in the same AnyIO event loop. - """ - - def __init__( - self, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - ) -> None: - """Initialize the lock backend.""" - self._locks: dict[str, tuple[str | None, float]] = {} - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Enter the lock backend.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Exit the lock backend.""" - self._locks.clear() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - if ( - current_token is None - or current_token == token - or expire_at < monotonic() - ): - self._locks[name] = (token, monotonic() + duration) - return True - return False - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - if current_token == token and expire_at >= monotonic(): - del self._locks[name] - return True - if current_token and expire_at < monotonic(): - del self._locks[name] - return False - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - return current_token is not None and expire_at >= monotonic() - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - current_token, expire_at = self._locks.get(name, (None, 0)) - return current_token == token and expire_at >= monotonic() diff --git a/.conflict-side-1/grelmicro/sync/postgres.py b/.conflict-side-1/grelmicro/sync/postgres.py deleted file mode 100644 index 451cc0c..0000000 --- a/.conflict-side-1/grelmicro/sync/postgres.py +++ /dev/null @@ -1,206 +0,0 @@ -"""PostgreSQL Synchronization Backend.""" - -from types import TracebackType -from typing import Annotated, Self - -from asyncpg import Pool, create_pool -from pydantic import PostgresDsn -from pydantic_core import MultiHostUrl, ValidationError -from pydantic_settings import BaseSettings -from typing_extensions import Doc - -from grelmicro.errors import OutOfContextError -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import SyncSettingsValidationError - - -class _PostgresSettings(BaseSettings): - """PostgreSQL settings from the environment variables.""" - - POSTGRES_HOST: str | None = None - POSTGRES_PORT: int = 5432 - POSTGRES_DB: str | None = None - POSTGRES_USER: str | None = None - POSTGRES_PASSWORD: str | None = None - POSTGRES_URL: PostgresDsn | None = None - - -def _get_postgres_url() -> str: - """Get the PostgreSQL URL from the environment variables. - - Raises: - SyncSettingsValidationError: If the URL or all of the host, database, user, and password - """ - try: - settings = _PostgresSettings() - except ValidationError as error: - raise SyncSettingsValidationError(error) from None - - required_parts = [ - settings.POSTGRES_HOST, - settings.POSTGRES_DB, - settings.POSTGRES_USER, - settings.POSTGRES_PASSWORD, - ] - - if settings.POSTGRES_URL and not any(required_parts): - return settings.POSTGRES_URL.unicode_string() - - if all(required_parts) and not settings.POSTGRES_URL: - return MultiHostUrl.build( - scheme="postgresql", - username=settings.POSTGRES_USER, - password=settings.POSTGRES_PASSWORD, - host=settings.POSTGRES_HOST, - port=settings.POSTGRES_PORT, - path=settings.POSTGRES_DB, - ).unicode_string() - - msg = ( - "Either POSTGRES_URL or all of POSTGRES_HOST, POSTGRES_DB, POSTGRES_USER, and " - "POSTGRES_PASSWORD must be set" - ) - raise SyncSettingsValidationError(msg) - - -class PostgresSyncBackend(SyncBackend): - """PostgreSQL Synchronization Backend.""" - - _SQL_CREATE_TABLE_IF_NOT_EXISTS = """ - CREATE TABLE IF NOT EXISTS {table_name} ( - name TEXT PRIMARY KEY, - token TEXT NOT NULL, - expire_at TIMESTAMP NOT NULL - ); - """ - - _SQL_ACQUIRE_OR_EXTEND = """ - INSERT INTO {table_name} (name, token, expire_at) - VALUES ($1, $2, NOW() + make_interval(secs => $3)) - ON CONFLICT (name) DO UPDATE - SET token = EXCLUDED.token, expire_at = EXCLUDED.expire_at - WHERE {table_name}.token = EXCLUDED.token OR {table_name}.expire_at < NOW() - RETURNING 1; - """ - - _SQL_RELEASE = """ - DELETE FROM {table_name} - WHERE name = $1 AND token = $2 AND expire_at >= NOW() - RETURNING 1; - """ - - _SQL_RELEASE_ALL_EXPIRED = """ - DELETE FROM {table_name} - WHERE expire_at < NOW(); - """ - - _SQL_LOCKED = """ - SELECT 1 FROM {table_name} - WHERE name = $1 AND expire_at >= NOW(); - """ - - _SQL_OWNED = """ - SELECT 1 FROM {table_name} - WHERE name = $1 AND token = $2 AND expire_at >= NOW(); - """ - - def __init__( - self, - url: Annotated[ - PostgresDsn | str | None, - Doc(""" - The Postgres database URL. - - If not provided, the URL will be taken from the environment variables POSTGRES_URL - or POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD. - """), - ] = None, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - table_name: Annotated[ - str, Doc("The table name to store the locks.") - ] = "locks", - ) -> None: - """Initialize the lock backend.""" - if not table_name.isidentifier(): - msg = f"Table name '{table_name}' is not a valid identifier" - raise ValueError(msg) - - self._url = url or _get_postgres_url() - self._table_name = table_name - self._acquire_sql = self._SQL_ACQUIRE_OR_EXTEND.format( - table_name=table_name - ) - self._release_sql = self._SQL_RELEASE.format(table_name=table_name) - self._pool: Pool | None = None - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Enter the lock backend.""" - self._pool = await create_pool(str(self._url)) - await self._pool.execute( - self._SQL_CREATE_TABLE_IF_NOT_EXISTS.format( - table_name=self._table_name - ), - ) - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Exit the lock backend.""" - if self._pool: - await self._pool.execute( - self._SQL_RELEASE_ALL_EXPIRED.format( - table_name=self._table_name - ), - ) - await self._pool.close() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire a lock.""" - if not self._pool: - raise OutOfContextError(self, "acquire") - - return bool( - await self._pool.fetchval(self._acquire_sql, name, token, duration) - ) - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - if not self._pool: - raise OutOfContextError(self, "release") - return bool(await self._pool.fetchval(self._release_sql, name, token)) - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - if not self._pool: - raise OutOfContextError(self, "locked") - return bool( - await self._pool.fetchval( - self._SQL_LOCKED.format(table_name=self._table_name), - name, - ), - ) - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - if not self._pool: - raise OutOfContextError(self, "owned") - return bool( - await self._pool.fetchval( - self._SQL_OWNED.format(table_name=self._table_name), - name, - token, - ), - ) diff --git a/.conflict-side-1/grelmicro/sync/redis.py b/.conflict-side-1/grelmicro/sync/redis.py deleted file mode 100644 index 73090c8..0000000 --- a/.conflict-side-1/grelmicro/sync/redis.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Redis Synchronization Backend.""" - -from types import TracebackType -from typing import Annotated, Self - -from pydantic import RedisDsn, ValidationError -from pydantic_core import Url -from pydantic_settings import BaseSettings -from redis.asyncio.client import Redis -from typing_extensions import Doc - -from grelmicro.sync._backends import loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import SyncSettingsValidationError - - -class _RedisSettings(BaseSettings): - """Redis settings from the environment variables.""" - - REDIS_HOST: str | None = None - REDIS_PORT: int = 6379 - REDIS_DB: int = 0 - REDIS_PASSWORD: str | None = None - REDIS_URL: RedisDsn | None = None - - -def _get_redis_url() -> str: - """Get the Redis URL from the environment variables. - - Raises: - SyncSettingsValidationError: If the URL or host is not set. - """ - try: - settings = _RedisSettings() - except ValidationError as error: - raise SyncSettingsValidationError(error) from None - - if settings.REDIS_URL and not settings.REDIS_HOST: - return settings.REDIS_URL.unicode_string() - - if settings.REDIS_HOST and not settings.REDIS_URL: - return Url.build( - scheme="redis", - host=settings.REDIS_HOST, - port=settings.REDIS_PORT, - path=str(settings.REDIS_DB), - password=settings.REDIS_PASSWORD, - ).unicode_string() - - msg = "Either REDIS_URL or REDIS_HOST must be set" - raise SyncSettingsValidationError(msg) - - -class RedisSyncBackend(SyncBackend): - """Redis Synchronization Backend.""" - - _LUA_ACQUIRE_OR_EXTEND = """ - local token = redis.call('get', KEYS[1]) - if not token then - redis.call('set', KEYS[1], ARGV[1], 'px', ARGV[2]) - return 1 - end - if token == ARGV[1] then - redis.call('pexpire', KEYS[1], ARGV[2]) - return 1 - end - return 0 - """ - _LUA_RELEASE = """ - local token = redis.call('get', KEYS[1]) - if not token or token ~= ARGV[1] then - return 0 - end - redis.call('del', KEYS[1]) - return 1 - """ - - def __init__( - self, - url: Annotated[ - RedisDsn | str | None, - Doc(""" - The Redis URL. - - If not provided, the URL will be taken from the environment variables REDIS_URL - or REDIS_HOST, REDIS_PORT, REDIS_DB, and REDIS_PASSWORD. - """), - ] = None, - *, - auto_register: Annotated[ - bool, - Doc( - "Automatically register the lock backend in the backend registry." - ), - ] = True, - ) -> None: - """Initialize the lock backend.""" - self._url = url or _get_redis_url() - self._redis: Redis = Redis.from_url(str(self._url)) - self._lua_release = self._redis.register_script(self._LUA_RELEASE) - self._lua_acquire = self._redis.register_script( - self._LUA_ACQUIRE_OR_EXTEND - ) - if auto_register: - loaded_backends["lock"] = self - - async def __aenter__(self) -> Self: - """Open the lock backend.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_value: BaseException | None, - traceback: TracebackType | None, - ) -> None: - """Close the lock backend.""" - await self._redis.aclose() - - async def acquire(self, *, name: str, token: str, duration: float) -> bool: - """Acquire the lock.""" - return bool( - await self._lua_acquire( - keys=[name], - args=[token, int(duration * 1000)], - client=self._redis, - ) - ) - - async def release(self, *, name: str, token: str) -> bool: - """Release the lock.""" - return bool( - await self._lua_release( - keys=[name], args=[token], client=self._redis - ) - ) - - async def locked(self, *, name: str) -> bool: - """Check if the lock is acquired.""" - return bool(await self._redis.get(name)) - - async def owned(self, *, name: str, token: str) -> bool: - """Check if the lock is owned.""" - return bool( - (await self._redis.get(name)) == token.encode() - ) # redis returns bytes diff --git a/.conflict-side-1/grelmicro/task/__init__.py b/.conflict-side-1/grelmicro/task/__init__.py deleted file mode 100644 index 374bf08..0000000 --- a/.conflict-side-1/grelmicro/task/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Grelmicro Task Scheduler.""" - -from grelmicro.task.manager import TaskManager -from grelmicro.task.router import TaskRouter - -__all__ = ["TaskManager", "TaskRouter"] diff --git a/.conflict-side-1/grelmicro/task/_interval.py b/.conflict-side-1/grelmicro/task/_interval.py deleted file mode 100644 index f66c2f2..0000000 --- a/.conflict-side-1/grelmicro/task/_interval.py +++ /dev/null @@ -1,92 +0,0 @@ -"""Interval Task.""" - -from collections.abc import Awaitable, Callable -from contextlib import nullcontext -from functools import partial -from inspect import iscoroutinefunction -from logging import getLogger -from typing import Any - -from anyio import TASK_STATUS_IGNORED, sleep, to_thread -from anyio.abc import TaskStatus -from fast_depends import inject - -from grelmicro.sync.abc import Synchronization -from grelmicro.task._utils import validate_and_generate_reference -from grelmicro.task.abc import Task - -logger = getLogger("grelmicro.task") - - -class IntervalTask(Task): - """Interval Task. - - Use the `TaskManager.interval()` or `SchedulerRouter.interval()` decorator instead - of creating IntervalTask objects directly. - """ - - def __init__( - self, - *, - function: Callable[..., Any], - name: str | None = None, - interval: float, - sync: Synchronization | None = None, - ) -> None: - """Initialize the IntervalTask. - - Raises: - FunctionNotSupportedError: If the function is not supported. - ValueError: If internal is less than or equal to 0. - """ - if interval <= 0: - msg = "Interval must be greater than 0" - raise ValueError(msg) - - alt_name = validate_and_generate_reference(function) - self._name = name or alt_name - self._interval = interval - self._async_function = self._prepare_async_function(function) - self._sync = sync if sync else nullcontext() - - @property - def name(self) -> str: - """Return the lock name.""" - return self._name - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run the repeated task loop.""" - logger.info( - "Task started (interval: %ss): %s", self._interval, self.name - ) - task_status.started() - try: - while True: - try: - async with self._sync: - try: - await self._async_function() - except Exception: - logger.exception( - "Task execution error: %s", self.name - ) - except Exception: - logger.exception( - "Task synchronization error: %s", self.name - ) - await sleep(self._interval) - finally: - logger.info("Task stopped: %s", self.name) - - def _prepare_async_function( - self, function: Callable[..., Any] - ) -> Callable[..., Awaitable[Any]]: - """Prepare the function with lock and ensure async function.""" - function = inject(function) - return ( - function - if iscoroutinefunction(function) - else partial(to_thread.run_sync, function) - ) diff --git a/.conflict-side-1/grelmicro/task/_utils.py b/.conflict-side-1/grelmicro/task/_utils.py deleted file mode 100644 index 7cfec3f..0000000 --- a/.conflict-side-1/grelmicro/task/_utils.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Task Utilities.""" - -from collections.abc import Callable -from functools import partial -from inspect import ismethod -from typing import Any - -from grelmicro.task.errors import FunctionTypeError - - -def validate_and_generate_reference(function: Callable[..., Any]) -> str: - """Generate a task name from the given function. - - This implementation is inspirated by the APScheduler project under MIT License. - Original source: https://github.com/agronholm/apscheduler/blob/master/src/apscheduler/_marshalling.py - - Raises: - FunctionNotSupportedError: If function is not supported. - - """ - if isinstance(function, partial): - ref = "partial()" - raise FunctionTypeError(ref) - - if ismethod(function): - ref = "method" - raise FunctionTypeError(ref) - - if not hasattr(function, "__module__") or not hasattr( - function, "__qualname__" - ): - ref = "callable without __module__ or __qualname__ attribute" - raise FunctionTypeError(ref) - - if "" in function.__qualname__: - ref = "lambda" - raise FunctionTypeError(ref) - - if "" in function.__qualname__: - ref = "nested function" - raise FunctionTypeError(ref) - - return f"{function.__module__}:{function.__qualname__}" diff --git a/.conflict-side-1/grelmicro/task/abc.py b/.conflict-side-1/grelmicro/task/abc.py deleted file mode 100644 index d4e7cf3..0000000 --- a/.conflict-side-1/grelmicro/task/abc.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Grelmicro Task Synchronization Abstract Base Classes and Protocols.""" - -from typing import Protocol - -from anyio import TASK_STATUS_IGNORED -from anyio.abc import TaskStatus -from typing_extensions import runtime_checkable - - -@runtime_checkable -class Task(Protocol): - """Task Protocol. - - A task that runs in background in the async event loop. - """ - - @property - def name(self) -> str: - """Name to uniquely identify the task.""" - ... - - async def __call__( - self, - *, - task_status: TaskStatus[None] = TASK_STATUS_IGNORED, - ) -> None: - """Run the task. - - This is the entry point of the task to be run in the async event loop. - """ - ... diff --git a/.conflict-side-1/grelmicro/task/errors.py b/.conflict-side-1/grelmicro/task/errors.py deleted file mode 100644 index a788f61..0000000 --- a/.conflict-side-1/grelmicro/task/errors.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Grelmicro Task Scheduler Errors.""" - -from grelmicro.errors import GrelmicroError - - -class TaskError(GrelmicroError): - """Base Grelmicro Task error.""" - - -class FunctionTypeError(TaskError, TypeError): - """Function Type Error.""" - - def __init__(self, reference: str) -> None: - """Initialize the error.""" - super().__init__( - f"Could not use function {reference}, " - "try declaring 'def' or 'async def' directly in the module" - ) - - -class TaskAddOperationError(TaskError, RuntimeError): - """Task Add Operation Error.""" - - def __init__(self) -> None: - """Initialize the error.""" - super().__init__( - "Could not add the task, try calling 'add_task' and 'include_router' before starting" - ) diff --git a/.conflict-side-1/grelmicro/task/manager.py b/.conflict-side-1/grelmicro/task/manager.py deleted file mode 100644 index 5432145..0000000 --- a/.conflict-side-1/grelmicro/task/manager.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Grelmicro Task Manager.""" - -from contextlib import AsyncExitStack -from logging import getLogger -from types import TracebackType -from typing import TYPE_CHECKING, Annotated, Self - -from anyio import create_task_group -from typing_extensions import Doc - -from grelmicro.errors import OutOfContextError -from grelmicro.task.abc import Task -from grelmicro.task.errors import TaskAddOperationError -from grelmicro.task.router import TaskRouter - -if TYPE_CHECKING: - from anyio.abc import TaskGroup - -logger = getLogger("grelmicro.task") - - -class TaskManager(TaskRouter): - """Task Manager. - - `TaskManager` class, the main entrypoint to manage scheduled tasks. - """ - - def __init__( - self, - *, - auto_start: Annotated[ - bool, - Doc( - """ - Automatically start all tasks. - """, - ), - ] = True, - tasks: Annotated[ - list[Task] | None, - Doc( - """ - A list of tasks to be started. - """, - ), - ] = None, - ) -> None: - """Initialize the task manager.""" - TaskRouter.__init__(self, tasks=tasks) - - self._auto_start = auto_start - self._task_group: TaskGroup | None = None - - async def __aenter__(self) -> Self: - """Enter the context manager.""" - self._exit_stack = AsyncExitStack() - await self._exit_stack.__aenter__() - self._task_group = await self._exit_stack.enter_async_context( - create_task_group(), - ) - if self._auto_start: - await self.start() - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the context manager.""" - if not self._task_group or not self._exit_stack: - raise OutOfContextError(self, "__aexit__") - self._task_group.cancel_scope.cancel() - return await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb) - - async def start(self) -> None: - """Start all tasks manually.""" - if not self._task_group: - raise OutOfContextError(self, "start") - - if self._started: - raise TaskAddOperationError - - self.do_mark_as_started() - - for task in self.tasks: - await self._task_group.start(task.__call__) - logger.debug("%s scheduled tasks started", len(self._tasks)) diff --git a/.conflict-side-1/grelmicro/task/router.py b/.conflict-side-1/grelmicro/task/router.py deleted file mode 100644 index 16b240d..0000000 --- a/.conflict-side-1/grelmicro/task/router.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Grelmicro Task Router.""" - -from collections.abc import Awaitable, Callable -from typing import Annotated, Any - -from typing_extensions import Doc - -from grelmicro.sync.abc import Synchronization -from grelmicro.task.abc import Task -from grelmicro.task.errors import TaskAddOperationError - - -class TaskRouter: - """Task Router. - - `TaskRouter` class, used to group task schedules, for example to structure an app in - multiple files. It would then included in the `TaskManager`, or in another - `TaskRouter`. - """ - - def __init__( - self, - *, - tasks: Annotated[ - list[Task] | None, - Doc( - """ - A list of schedules or scheduled tasks to be scheduled. - """, - ), - ] = None, - ) -> None: - """Initialize the task router.""" - self._started = False - self._tasks: list[Task] = tasks or [] - self._routers: list[TaskRouter] = [] - - @property - def tasks(self) -> list[Task]: - """List of scheduled tasks.""" - return self._tasks + [ - task for router in self._routers for task in router.tasks - ] - - def add_task(self, task: Task) -> None: - """Add a task to the scheduler.""" - if self._started: - raise TaskAddOperationError - - self._tasks.append(task) - - def interval( - self, - *, - seconds: Annotated[ - float, - Doc( - """ - The duration in seconds between each task run. - - Accuracy is not guaranteed and may vary with system load. Consider the - execution time of the task when setting the interval. - """, - ), - ], - name: Annotated[ - str | None, - Doc( - """ - The name of the task. - - If None, a name will be generated automatically from the function. - """, - ), - ] = None, - sync: Annotated[ - Synchronization | None, - Doc( - """ - The synchronization primitive to use for the task. - - You can use a `LeasedLock` or a `LeaderElection`, for example. If None, - no synchronization is used and the task will run on all workers. - """, - ), - ] = None, - ) -> Callable[ - [Callable[..., Any | Awaitable[Any]]], - Callable[..., Any | Awaitable[Any]], - ]: - """Decorate function to add it to the task scheduler. - - Raises: - TaskNameGenerationError: If the task name generation fails. - """ - from grelmicro.task._interval import IntervalTask - - def decorator( - function: Callable[[], None | Awaitable[None]], - ) -> Callable[[], None | Awaitable[None]]: - self.add_task( - IntervalTask( - name=name, - function=function, - interval=seconds, - sync=sync, - ), - ) - return function - - return decorator - - def include_router(self, router: "TaskRouter") -> None: - """Include another router in this router.""" - if self._started: - raise TaskAddOperationError - - self._routers.append(router) - - def started(self) -> bool: - """Check if the task manager has started.""" - return self._started - - def do_mark_as_started(self) -> None: - """Mark the task manager as started. - - Do not call this method directly. It is called by the task manager when the task - manager is started. - """ - self._started = True - for router in self._routers: - router.do_mark_as_started() diff --git a/.conflict-side-1/mkdocs.yml b/.conflict-side-1/mkdocs.yml deleted file mode 100644 index 0b08e9f..0000000 --- a/.conflict-side-1/mkdocs.yml +++ /dev/null @@ -1,47 +0,0 @@ -site_name: Grelmicro -site_description: Grelmicro is a lightweight framework/toolkit which is ideal for building async microservices in Python. -site_url: https://grelmicro.grel.info -theme: - name: material - palette: - primary: green - accent: light green - font: - text: 'Roboto' - code: 'Roboto Mono' - features: - - content.tabs.link - - content.code.copy - - content.code.select - - content.tooltips - - navigation.indexes - - navigation.instant - - navigation.instant.prefetch - - navigation.instant.progress - - navigation.top - - navigation.tracking - -repo_name: grelinfo/grelmicro -repo_url: https://github.com/grelinfo/grelmicro - -validation: - omitted_files: warn - absolute_links: warn - unrecognized_links: warn - -nav: -- Grelmicro: index.md -- User Guide: - - logging.md - - sync.md - - task.md - -markdown_extensions: - - admonition - - mdx_include: - base_path: docs - - pymdownx.highlight - - pymdownx.superfences - - pymdownx.inlinehilite - - pymdownx.tabbed: - alternate_style: true diff --git a/.conflict-side-1/pyproject.toml b/.conflict-side-1/pyproject.toml deleted file mode 100644 index 9bcca87..0000000 --- a/.conflict-side-1/pyproject.toml +++ /dev/null @@ -1,174 +0,0 @@ -[project] -name = "grelmicro" -description = "Grelmicro is a lightweight framework/toolkit for building async microservices in Python" -license = "MIT" -authors = [{ name = "Loïc Gremaud", email = "grelinfo@gmail.com"}] -readme = "README.md" - -classifiers = [ - "Intended Audience :: Information Technology", - "Intended Audience :: System Administrators", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3", - "Programming Language :: Python", - "Topic :: Internet", - "Topic :: Software Development :: Libraries :: Application Frameworks", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Libraries", - "Topic :: Software Development", - "Typing :: Typed", - "Development Status :: 1 - Planning", - "Environment :: Web Environment", - "Framework :: AsyncIO", - "Framework :: FastAPI", - "Framework :: Pydantic", - "Framework :: Pydantic :: 2", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", -] -dynamic = ["version"] - -requires-python = ">=3.11" - -dependencies = [ - "anyio>=4.0.0", - "pydantic>=2.5.0", - "fast-depends>=2.0.0", - "pydantic-settings>=2.5.0", -] - -[project.urls] - -Repository = "https://github.com/grelinfo/grelmicro.git" -Issues = "https://github.com/grelinfo/grelmicro/issues" - -[project.optional-dependencies] -standard = [ - "loguru>=0.7.2", - "orjson>=3.10.11", -] -postgres = [ - "asyncpg>=0.30.0", -] -redis = [ - "redis>=5.0.0", -] - -[dependency-groups] -dev = [ - "pytest-cov>=6.0.0", - "pytest>=8.0.0", - "mypy>=1.12.0", - "ruff>=0.7.4", - "testcontainers[postgres,redis]>=4.8.2", - "pytest-timeout>=2.3.1", - "pytest-mock>=3.14.0", - "pytest-randomly>=3.16.0", - "pre-commit>=4.0.1", - "fastapi>=0.115.5", - "fastapi-cli>=0.0.5", - "mdx-include>=1.4.2", - "faststream>=0.5.30", - "hatch>=1.13.0", -] -docs = [ - "mkdocs-material>=9.5.44", - "pygments>=2.18.0", - "pymdown-extensions>=10.12", -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build] -skip-excluded-dirs = true -exclude = ["/tests", "/docs", "/examples"] - -[tool.hatch.version] -path = "grelmicro/__init__.py" - -[tool.ruff] -target-version = "py311" -line-length = 80 - -[tool.ruff.lint] -select = ["ALL"] -ignore = ["COM812", "ISC001"] # Ignore rules conflicting with the formatter. - -[tool.ruff.lint.extend-per-file-ignores] -"examples/*" = [ - "ARG001", - "ANN001", - "ANN201", - "D103", - "D100", - "INP001", - "T201", -] -"examples/logging/basic.py" = ["EM101", "TRY"] -"examples/task/router.py" = ["I001", "E402"] -"tests/*" = [ - "S101", - "SLF001" -] - -[tool.ruff.lint.pycodestyle] -max-line-length = 100 # reports only line that exceed 100 characters. - -[tool.ruff.lint.pydocstyle] -convention = "pep257" - -[tool.ruff.lint.pylint] -max-args = 10 - -[tool.mypy] -scripts_are_modules = true -plugins = [ - "pydantic.mypy" -] -follow_imports = "silent" -warn_redundant_casts = true -warn_unused_ignores = true -disallow_any_generics = true -check_untyped_defs = true -no_implicit_reexport = true -disallow_untyped_defs = true - -[[tool.mypy.overrides]] -module = ["asyncpg", "testcontainers.*"] -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = [ - "examples.*" -] -disallow_untyped_defs = false - - -[tool.pytest.ini_options] -addopts = """ - --cov=grelmicro - --cov-report term:skip-covered - --cov-report xml:cov.xml - --strict-config - --strict-markers - -m "not integration" -""" -markers = """ - integration: mark a test as an integration test (disabled by default). -""" - -testpaths = "tests" - -[tool.coverage.report] -sort = "-Cover" -exclude_also = [ - "if TYPE_CHECKING:", - "class .*\\bProtocol\\):", - "assert_never\\(.*\\)", -] diff --git a/.conflict-side-1/tests/__init__.py b/.conflict-side-1/tests/__init__.py deleted file mode 100644 index adc28b2..0000000 --- a/.conflict-side-1/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Tests.""" diff --git a/.conflict-side-1/tests/conftest.py b/.conflict-side-1/tests/conftest.py deleted file mode 100644 index 916c148..0000000 --- a/.conflict-side-1/tests/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Grelmicro Test Config.""" - -import pytest - - -@pytest.fixture -def anyio_backend() -> str: - """AnyIO Backend.""" - return "asyncio" diff --git a/.conflict-side-1/tests/logging/__init__.py b/.conflict-side-1/tests/logging/__init__.py deleted file mode 100644 index a1c677a..0000000 --- a/.conflict-side-1/tests/logging/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Logging Tests.""" diff --git a/.conflict-side-1/tests/logging/test_loguru.py b/.conflict-side-1/tests/logging/test_loguru.py deleted file mode 100644 index 9214250..0000000 --- a/.conflict-side-1/tests/logging/test_loguru.py +++ /dev/null @@ -1,274 +0,0 @@ -"""Test Logging Loguru.""" - -from collections.abc import Generator -from datetime import datetime -from io import StringIO - -import pytest -from loguru import logger -from pydantic import TypeAdapter - -from grelmicro.errors import DependencyNotFoundError -from grelmicro.logging.errors import LoggingSettingsValidationError -from grelmicro.logging.loguru import ( - JSON_FORMAT, - JSONRecordDict, - configure_logging, - json_formatter, - json_patcher, -) - -json_record_type_adapter = TypeAdapter(JSONRecordDict) - - -@pytest.fixture(autouse=True) -def cleanup_handlers() -> Generator[None, None, None]: - """Cleanup logging handlers.""" - logger.configure(handlers=[]) - yield - logger.remove() - - -def generate_logs() -> int: - """Generate logs.""" - logger.debug("Hello, World!") - logger.info("Hello, World!") - logger.warning("Hello, World!") - logger.error("Hello, Alice!", user="Alice") - try: - 1 / 0 # noqa: B018 - except ZeroDivisionError: - logger.exception("Hello, Bob!") - - return 5 - - -def assert_logs(logs: str) -> None: - """Assert logs.""" - ( - info, - warning, - error, - exception, - ) = ( - json_record_type_adapter.validate_json(line) - for line in logs.splitlines()[0:4] - ) - - expected_separator = 3 - - assert info["logger"] - assert info["logger"].startswith("tests.logging.test_loguru:generate_logs:") - assert len(info["logger"].split(":")) == expected_separator - assert info["time"] == datetime.fromisoformat(info["time"]).isoformat() - assert info["level"] == "INFO" - assert info["msg"] == "Hello, World!" - assert info["thread"] == "MainThread" - assert "ctx" not in info - - assert warning["logger"] - assert warning["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(warning["logger"].split(":")) == expected_separator - assert ( - warning["time"] == datetime.fromisoformat(warning["time"]).isoformat() - ) - assert warning["level"] == "WARNING" - assert warning["msg"] == "Hello, World!" - assert warning["thread"] == "MainThread" - assert "ctx" not in warning - - assert error["logger"] - assert error["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(error["logger"].split(":")) == expected_separator - assert error["time"] == datetime.fromisoformat(error["time"]).isoformat() - assert error["level"] == "ERROR" - assert error["msg"] == "Hello, Alice!" - assert error["thread"] == "MainThread" - assert error["ctx"] == {"user": "Alice"} - - assert exception["logger"] - assert exception["logger"].startswith( - "tests.logging.test_loguru:generate_logs:" - ) - assert len(exception["logger"].split(":")) == expected_separator - assert ( - exception["time"] - == datetime.fromisoformat(exception["time"]).isoformat() - ) - assert exception["level"] == "ERROR" - assert exception["msg"] == "Hello, Bob!" - assert exception["thread"] == "MainThread" - assert exception["ctx"] == { - "exception": "ZeroDivisionError: division by zero", - } - - -def test_json_formatter() -> None: - """Test JSON Formatter.""" - # Arrange - sink = StringIO() - - # Act - logger.add(sink, format=json_formatter, level="INFO") - generate_logs() - - # Assert - assert_logs(sink.getvalue()) - - -def test_json_patching() -> None: - """Test JSON Patching.""" - # Arrange - sink = StringIO() - - # Act - # logger.patch(json_patcher) -> Patch is not working using logger.configure instead - logger.configure(patcher=json_patcher) - logger.add(sink, format=JSON_FORMAT, level="INFO") - generate_logs() - - # Assert - assert_logs(sink.getvalue()) - - -def test_configure_logging_default( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Default.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - configure_logging() - generate_logs() - - # Assert - assert_logs(capsys.readouterr().out) - - -def test_configure_logging_text( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Text.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "text") - - # Act - configure_logging() - generate_logs() - - # Assert - lines = capsys.readouterr().out.splitlines() - - assert "tests.logging.test_loguru:generate_logs:" in lines[0] - assert " | INFO | " in lines[0] - assert " - Hello, World!" in lines[0] - - assert "tests.logging.test_loguru:generate_logs:" in lines[1] - assert " | WARNING | " in lines[1] - assert " - Hello, World!" in lines[1] - - assert "tests.logging.test_loguru:generate_logs:" in lines[2] - assert " | ERROR | " in lines[2] - assert " - Hello, Alice!" in lines[2] - - assert "tests.logging.test_loguru:generate_logs:" in lines[3] - assert " | ERROR | " in lines[3] - assert " - Hello, Bob!" in lines[3] - assert "Traceback" in lines[4] - assert "ZeroDivisionError: division by zero" in lines[-1] - - -def test_configure_logging_json( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging JSON.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "json") - - # Act - configure_logging() - generate_logs() - - # Assert - assert_logs(capsys.readouterr().out) - - -def test_configure_logging_level( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Level.""" - # Arrange - monkeypatch.setenv("LOG_LEVEL", "DEBUG") - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - configure_logging() - logs_count = generate_logs() - - # Assert - assert len(capsys.readouterr().out.splitlines()) == logs_count - - -def test_configure_logging_invalid_level( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Invalid Level.""" - # Arrange - monkeypatch.setenv("LOG_LEVEL", "INVALID") - monkeypatch.delenv("LOG_FORMAT", raising=False) - - # Act - with pytest.raises( - LoggingSettingsValidationError, - match=( - r"Could not validate environment variables settings:\n" - r"- LOG_LEVEL: Input should be 'DEBUG', 'INFO', 'WARNING', 'ERROR' or 'CRITICAL'" - r" \[input=INVALID\]" - ), - ): - configure_logging() - - # Assert - assert not capsys.readouterr().out - - -def test_configure_logging_format_template( - capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Configure Logging Format Template.""" - # Arrange - monkeypatch.delenv("LOG_LEVEL", raising=False) - monkeypatch.setenv("LOG_FORMAT", "{level}: {message}") - - # Act - configure_logging() - generate_logs() - - # Assert - lines = capsys.readouterr().out.splitlines() - assert "INFO: Hello, World!" in lines[0] - assert "WARNING: Hello, World!" in lines[1] - assert "ERROR: Hello, Alice!" in lines[2] - assert "ERROR: Hello, Bob!" in lines[3] - assert "Traceback" in lines[4] - assert "ZeroDivisionError: division by zero" in lines[-1] - - -def test_configure_logging_dependency_not_found( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Test Configure Logging Dependency Not Found.""" - # Arrange - monkeypatch.setattr("grelmicro.logging.loguru.loguru", None) - - # Act / Assert - with pytest.raises(DependencyNotFoundError, match="loguru"): - configure_logging() diff --git a/.conflict-side-1/tests/sync/__init__.py b/.conflict-side-1/tests/sync/__init__.py deleted file mode 100644 index 5e3b5c4..0000000 --- a/.conflict-side-1/tests/sync/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Synchronization Primitives Tests.""" diff --git a/.conflict-side-1/tests/sync/test_backends.py b/.conflict-side-1/tests/sync/test_backends.py deleted file mode 100644 index b08a92f..0000000 --- a/.conflict-side-1/tests/sync/test_backends.py +++ /dev/null @@ -1,370 +0,0 @@ -"""Test Synchronization Backends.""" - -from collections.abc import AsyncGenerator, Callable, Generator -from uuid import uuid4 - -import pytest -from anyio import sleep -from testcontainers.core.container import DockerContainer -from testcontainers.postgres import PostgresContainer -from testcontainers.redis import RedisContainer - -from grelmicro.sync._backends import get_sync_backend, loaded_backends -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import BackendNotLoadedError -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.sync.postgres import PostgresSyncBackend -from grelmicro.sync.redis import RedisSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(15)] - - -@pytest.fixture(scope="module") -def anyio_backend() -> str: - """AnyIO Backend Module Scope.""" - return "asyncio" - - -@pytest.fixture(scope="module") -def monkeypatch() -> Generator[pytest.MonkeyPatch, None, None]: - """Monkeypatch Module Scope.""" - monkeypatch = pytest.MonkeyPatch() - yield monkeypatch - monkeypatch.undo() - - -@pytest.fixture -def clean_registry() -> Generator[None, None, None]: - """Make sure the registry is clean.""" - loaded_backends.pop("lock", None) - yield - loaded_backends.pop("lock", None) - - -@pytest.fixture( - params=[ - "memory", - pytest.param("redis", marks=[pytest.mark.integration]), - pytest.param("postgres", marks=[pytest.mark.integration]), - ], - scope="module", -) -def backend_name(request: pytest.FixtureRequest) -> str: - """Backend Name.""" - return request.param - - -@pytest.fixture( - scope="module", -) -def container( - backend_name: str, - monkeypatch: pytest.MonkeyPatch, -) -> Generator[DockerContainer | None, None, None]: - """Test Container for each Backend.""" - if backend_name == "redis": - with RedisContainer() as container: - yield container - elif backend_name == "postgres": - monkeypatch.setenv("POSTGRES_HOST", "localhost") - monkeypatch.setenv("POSTGRES_PORT", "5432") - monkeypatch.setenv("POSTGRES_DB", "test") - monkeypatch.setenv("POSTGRES_USER", "test") - monkeypatch.setenv("POSTGRES_PASSWORD", "test") - with PostgresContainer() as container: - yield container - elif backend_name == "memory": - yield None - - -@pytest.fixture(scope="module") -async def backend( - backend_name: str, container: DockerContainer | None -) -> AsyncGenerator[SyncBackend]: - """Test Container for each Backend.""" - if backend_name == "redis" and container: - port = container.get_exposed_port(6379) - async with RedisSyncBackend(f"redis://localhost:{port}/0") as backend: - yield backend - elif backend_name == "postgres" and container: - port = container.get_exposed_port(5432) - async with PostgresSyncBackend( - f"postgresql://test:test@localhost:{port}/test" - ) as backend: - yield backend - elif backend_name == "memory": - async with MemorySyncBackend() as backend: - yield backend - - -async def test_acquire(backend: SyncBackend) -> None: - """Test acquire.""" - # Arrange - name = "test_acquire" - token = uuid4().hex - duration = 1 - - # Act - result = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result - - -async def test_acquire_reantrant(backend: SyncBackend) -> None: - """Test acquire is reantrant.""" - # Arrange - name = "test_acquire_reantrant" - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result1 - assert result2 - - -async def test_acquire_already_acquired(backend: SyncBackend) -> None: - """Test acquire when already acquired.""" - # Arrange - name = "test_acquire_already_acquired" - token1 = uuid4().hex - token2 = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token1, duration=duration) - result2 = await backend.acquire(name=name, token=token2, duration=duration) - - # Assert - assert token1 != token2 - assert result1 - assert not result2 - - -async def test_acquire_expired(backend: SyncBackend) -> None: - """Test acquire when expired.""" - # Arrange - name = "test_acquire_expired" - token = uuid4().hex - duration = 0.01 - - # Act - result = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.acquire(name=name, token=token, duration=duration) - - # Assert - assert result - assert result2 - - -async def test_acquire_already_acquired_expired(backend: SyncBackend) -> None: - """Test acquire when already acquired but expired.""" - # Arrange - name = "test_acquire_already_acquired_expired" + uuid4().hex - token1 = uuid4().hex - token2 = uuid4().hex - duration = 0.01 - - # Act - result = await backend.acquire(name=name, token=token1, duration=duration) - await sleep(duration * 2) - result2 = await backend.acquire(name=name, token=token2, duration=duration) - - # Assert - assert token1 != token2 - assert result - assert result2 - - -async def test_release_not_acquired(backend: SyncBackend) -> None: - """Test release when not acquired.""" - # Arrange - name = "test_release" + uuid4().hex - token = uuid4().hex - - # Act - result = await backend.release(name=name, token=token) - - # Assert - assert not result - - -async def test_release_acquired(backend: SyncBackend) -> None: - """Test release when acquired.""" - # Arrange - name = "test_release_acquired" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert result2 - - -async def test_release_not_reantrant(backend: SyncBackend) -> None: - """Test release is not reantrant.""" - # Arrange - name = "test_release_not_reantrant" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - result2 = await backend.release(name=name, token=token) - result3 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert result2 - assert not result3 - - -async def test_release_acquired_expired(backend: SyncBackend) -> None: - """Test release when acquired but expired.""" - # Arrange - name = "test_release_acquired_expired" + uuid4().hex - token = uuid4().hex - duration = 0.01 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert not result2 - - -async def test_release_not_acquired_expired(backend: SyncBackend) -> None: - """Test release when not acquired but expired.""" - # Arrange - name = "test_release_not_acquired_expired" + uuid4().hex - token = uuid4().hex - duration = 0.01 - - # Act - result1 = await backend.acquire(name=name, token=token, duration=duration) - await sleep(duration * 2) - result2 = await backend.release(name=name, token=token) - - # Assert - assert result1 - assert not result2 - - -async def test_locked(backend: SyncBackend) -> None: - """Test locked.""" - # Arrange - name = "test_locked" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - locked_before = await backend.locked(name=name) - await backend.acquire(name=name, token=token, duration=duration) - locked_after = await backend.locked(name=name) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_owned(backend: SyncBackend) -> None: - """Test owned.""" - # Arrange - name = "test_owned" + uuid4().hex - token = uuid4().hex - duration = 1 - - # Act - owned_before = await backend.owned(name=name, token=token) - await backend.acquire(name=name, token=token, duration=duration) - owned_after = await backend.owned(name=name, token=token) - - # Assert - assert owned_before is False - assert owned_after is True - - -async def test_owned_another(backend: SyncBackend) -> None: - """Test owned another.""" - # Arrange - name = "test_owned_another" + uuid4().hex - token1 = uuid4().hex - token2 = uuid4().hex - duration = 1 - - # Act - owned_before = await backend.owned(name=name, token=token1) - await backend.acquire(name=name, token=token1, duration=duration) - owned_after = await backend.owned(name=name, token=token2) - - # Assert - assert owned_before is False - assert owned_after is False - - -@pytest.mark.parametrize( - "backend_factory", - [ - lambda: MemorySyncBackend(), - lambda: RedisSyncBackend("redis://localhost:6379/0"), - lambda: PostgresSyncBackend( - "postgresql://user:password@localhost:5432/db" - ), - ], -) -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend(backend_factory: Callable[[], SyncBackend]) -> None: - """Test Get Synchronization Backend.""" - # Arrange - expected_backend = backend_factory() - - # Act - backend = get_sync_backend() - - # Assert - assert backend is expected_backend - - -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend_not_loaded() -> None: - """Test Get Synchronization Backend Not Loaded.""" - # Act / Assert - with pytest.raises(BackendNotLoadedError): - get_sync_backend() - - -@pytest.mark.parametrize( - "backend_factory", - [ - lambda: MemorySyncBackend(auto_register=False), - lambda: RedisSyncBackend( - "redis://localhost:6379/0", auto_register=False - ), - lambda: PostgresSyncBackend( - "postgresql://user:password@localhost:5432/db", auto_register=False - ), - ], -) -@pytest.mark.usefixtures("clean_registry") -def test_get_sync_backend_auto_register_disabled( - backend_factory: Callable[[], SyncBackend], -) -> None: - """Test Get Synchronization Backend.""" - # Arrange - backend_factory() - - # Act / Assert - with pytest.raises(BackendNotLoadedError): - get_sync_backend() diff --git a/.conflict-side-1/tests/sync/test_leaderelection.py b/.conflict-side-1/tests/sync/test_leaderelection.py deleted file mode 100644 index d357daa..0000000 --- a/.conflict-side-1/tests/sync/test_leaderelection.py +++ /dev/null @@ -1,457 +0,0 @@ -"""Test leader election.""" - -import math - -import pytest -from anyio import Event, create_task_group, sleep -from pydantic import ValidationError -from pytest_mock import MockerFixture - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.leaderelection import LeaderElection, LeaderElectionConfig -from grelmicro.sync.memory import MemorySyncBackend - -WORKERS = 4 -WORKER_1 = 0 -WORKER_2 = 1 -TEST_TIMEOUT = 1 - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(TEST_TIMEOUT)] - - -@pytest.fixture -def backend() -> SyncBackend: - """Return Memory Synchronization Backend.""" - return MemorySyncBackend() - - -@pytest.fixture -def configs() -> list[LeaderElectionConfig]: - """Leader election Config.""" - return [ - LeaderElectionConfig( - name="test_leader_election", - worker=f"worker_{i}", - lease_duration=0.02, - renew_deadline=0.015, - retry_interval=0.005, - error_interval=0.01, - backend_timeout=0.005, - ) - for i in range(WORKERS) - ] - - -@pytest.fixture -def leader_elections( - backend: SyncBackend, configs: list[LeaderElectionConfig] -) -> list[LeaderElection]: - """Leader elections.""" - return [ - LeaderElection(backend=backend, **configs[i].model_dump()) - for i in range(WORKERS) - ] - - -@pytest.fixture -def leader_election( - backend: SyncBackend, configs: list[LeaderElectionConfig] -) -> LeaderElection: - """Leader election.""" - return LeaderElection(backend=backend, **configs[WORKER_1].model_dump()) - - -async def wait_first_leader(leader_elections: list[LeaderElection]) -> None: - """Wait for the first leader to be elected.""" - - async def wrapper(leader_election: LeaderElection, event: Event) -> None: - """Wait for the leadership.""" - await leader_election.wait_for_leader() - event.set() - - async with create_task_group() as task_group: - event = Event() - for coroutine in leader_elections: - task_group.start_soon(wrapper, coroutine, event) - await event.wait() - task_group.cancel_scope.cancel() - - -def test_leader_election_config() -> None: - """Test leader election Config.""" - # Arrange - config = LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - lease_duration=0.01, - renew_deadline=0.008, - retry_interval=0.001, - error_interval=0.01, - backend_timeout=0.007, - ) - - # Assert - assert config.model_dump() == { - "name": "test_leader_election", - "worker": "worker_1", - "lease_duration": 0.01, - "renew_deadline": 0.008, - "retry_interval": 0.001, - "error_interval": 0.01, - "backend_timeout": 0.007, - } - - -def test_leader_election_config_defaults() -> None: - """Test leader election Config Defaults.""" - # Arrange - config = LeaderElectionConfig( - name="test_leader_election", worker="worker_1" - ) - - # Assert - assert config.model_dump() == { - "name": "test_leader_election", - "worker": "worker_1", - "lease_duration": 15, - "renew_deadline": 10, - "retry_interval": 2, - "error_interval": 30, - "backend_timeout": 5, - } - - -def test_leader_election_config_validation_errors() -> None: - """Test leader election Config Errors.""" - # Arrange - with pytest.raises( - ValidationError, - match="Renew deadline must be shorter than lease duration", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - lease_duration=15, - renew_deadline=20, - ) - with pytest.raises( - ValidationError, - match="Retry interval must be shorter than renew deadline", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - renew_deadline=10, - retry_interval=15, - ) - with pytest.raises( - ValidationError, - match="Backend timeout must be shorter than renew deadline", - ): - LeaderElectionConfig( - name="test_leader_election", - worker="worker_1", - renew_deadline=10, - backend_timeout=15, - ) - - -async def test_lifecycle(leader_election: LeaderElection) -> None: - """Test leader election on worker complete lifecycle.""" - # Act - is_leader_before_start = leader_election.is_leader() - is_running_before_start = leader_election.is_running() - async with create_task_group() as tg: - await tg.start(leader_election) - is_running_after_start = leader_election.is_running() - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - tg.cancel_scope.cancel() - is_running_after_cancel = leader_election.is_running() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_cancel is False - - assert is_running_before_start is False - assert is_running_after_start is True - assert is_running_after_cancel is False - - -async def test_leader_election_context_manager( - leader_election: LeaderElection, -) -> None: - """Test leader election on worker using context manager.""" - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - async with leader_election: - is_leader_inside_context = leader_election.is_leader() - is_leader_after_context = leader_election.is_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_inside_context is True - assert is_leader_after_context is True - assert is_leader_after_cancel is False - - -async def test_leader_election_single_worker( - leader_election: LeaderElection, -) -> None: - """Test leader election on single worker.""" - # Act - async with create_task_group() as tg: - is_leader_before_start = leader_election.is_leader() - await tg.start(leader_election) - is_leader_inside_context = leader_election.is_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - is_leader_after_cancel = leader_election.is_leader() - - # Assert - assert is_leader_before_start is False - assert is_leader_inside_context is True - assert is_leader_after_cancel is False - - -async def test_leadership_abandon_on_renew_deadline_reached( - leader_election: LeaderElection, -) -> None: - """Test leader election abandons leadership when renew deadline is reached.""" - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - leader_election.config.retry_interval = math.inf - await leader_election.wait_lose_leader() - is_leader_after_not_renewed = leader_election.is_leader() - tg.cancel_scope.cancel() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_not_renewed is False - - -async def test_leadership_abandon_on_backend_failure( - leader_election: LeaderElection, - caplog: pytest.LogCaptureFixture, - mocker: MockerFixture, -) -> None: - """Test leader election abandons leadership when backend is unreachable.""" - # Arrange - caplog.set_level("WARNING") - - # Act - is_leader_before_start = leader_election.is_leader() - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - is_leader_after_start = leader_election.is_leader() - mocker.patch.object( - leader_election.backend, - "acquire", - side_effect=Exception("Backend Unreachable"), - ) - await leader_election.wait_lose_leader() - is_leader_after_not_renewed = leader_election.is_leader() - tg.cancel_scope.cancel() - - # Assert - assert is_leader_before_start is False - assert is_leader_after_start is True - assert is_leader_after_not_renewed is False - assert ( - "Leader Election lost leadership: test_leader_election (renew deadline reached)" - in caplog.messages - ) - - -async def test_unepexpected_stop( - leader_election: LeaderElection, mocker: MockerFixture -) -> None: - """Test leader election worker abandons leadership on unexpected stop.""" - - # Arrange - async def leader_election_unexpected_exception() -> None: - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - mock = mocker.patch.object( - leader_election, - "_try_acquire_or_renew", - side_effect=Exception("Unexpected Exception"), - ) - await leader_election.wait_lose_leader() - mock.reset_mock() - tg.cancel_scope.cancel() - - # Act / Assert - with pytest.raises(ExceptionGroup): - await leader_election_unexpected_exception() - - -async def test_release_on_cancel( - backend: SyncBackend, leader_election: LeaderElection, mocker: MockerFixture -) -> None: - """Test leader election on worker that releases the lock on cancel.""" - # Arrange - spy_release = mocker.spy(backend, "release") - - # Act - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - - # Assert - spy_release.assert_called_once() - - -async def test_release_failure_ignored( - backend: SyncBackend, - leader_election: LeaderElection, - mocker: MockerFixture, -) -> None: - """Test leader election on worker that ignores release failure.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Unreachable") - ) - - # Act - async with create_task_group() as tg: - await tg.start(leader_election) - await leader_election.wait_for_leader() - tg.cancel_scope.cancel() - await leader_election.wait_lose_leader() - - -async def test_only_one_leader(leader_elections: list[LeaderElection]) -> None: - """Test leader election on multiple workers ensuring only one leader is elected.""" - # Act - leaders_before_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - async with create_task_group() as tg: - for leader_election in leader_elections: - await tg.start(leader_election) - await wait_first_leader(leader_elections) - leaders_after_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - tg.cancel_scope.cancel() - for leader_election in leader_elections: - await leader_election.wait_lose_leader() - leaders_after_cancel = [ - leader_election.is_leader() for leader_election in leader_elections - ] - - # Assert - assert sum(leaders_before_start) == 0 - assert sum(leaders_after_start) == 1 - assert sum(leaders_after_cancel) == 0 - - -async def test_leader_transition( - leader_elections: list[LeaderElection], -) -> None: - """Test leader election leader transition to another worker.""" - # Arrange - leaders_after_leader_election1_start = [False] * len(leader_elections) - leaders_after_all_start = [False] * len(leader_elections) - leaders_after_leader_election1_down = [False] * len(leader_elections) - - # Act - leaders_before_start = [ - leader_election.is_leader() for leader_election in leader_elections - ] - async with create_task_group() as workers_tg: - async with create_task_group() as worker1_tg: - await worker1_tg.start(leader_elections[WORKER_1]) - await leader_elections[WORKER_1].wait_for_leader() - leaders_after_leader_election1_start = [ - leader_election.is_leader() - for leader_election in leader_elections - ] - - for leader_election in leader_elections: - await workers_tg.start(leader_election) - leaders_after_all_start = [ - leader_election.is_leader() - for leader_election in leader_elections - ] - worker1_tg.cancel_scope.cancel() - - await leader_elections[WORKER_1].wait_lose_leader() - - await wait_first_leader(leader_elections) - leaders_after_leader_election1_down = [ - leader_election.is_leader() for leader_election in leader_elections - ] - workers_tg.cancel_scope.cancel() - - for leader_election in leader_elections[WORKER_2:]: - await leader_election.wait_lose_leader() - leaders_after_all_down = [ - leader_election.is_leader() for leader_election in leader_elections - ] - - # Assert - assert sum(leaders_before_start) == 0 - assert sum(leaders_after_leader_election1_start) == 1 - assert sum(leaders_after_all_start) == 1 - assert sum(leaders_after_leader_election1_down) == 1 - assert sum(leaders_after_all_down) == 0 - - assert leaders_after_leader_election1_start[WORKER_1] is True - assert leaders_after_leader_election1_down[WORKER_1] is False - - -async def test_error_interval( - backend: SyncBackend, - leader_elections: list[LeaderElection], - caplog: pytest.LogCaptureFixture, - mocker: MockerFixture, -) -> None: - """Test leader election on worker with error cooldown.""" - # Arrange - caplog.set_level("ERROR") - leader_elections[WORKER_1].config.error_interval = 1 - leader_elections[WORKER_2].config.error_interval = 0.001 - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Unreachable") - ) - - # Act - async with create_task_group() as tg: - await tg.start(leader_elections[WORKER_1]) - await sleep(0.01) - tg.cancel_scope.cancel() - leader_election1_nb_errors = sum( - 1 for record in caplog.records if record.levelname == "ERROR" - ) - caplog.clear() - - async with create_task_group() as tg: - await tg.start(leader_elections[WORKER_2]) - await sleep(0.01) - tg.cancel_scope.cancel() - leader_election2_nb_errors = sum( - 1 for record in caplog.records if record.levelname == "ERROR" - ) - - # Assert - assert leader_election1_nb_errors == 1 - assert leader_election2_nb_errors >= 1 diff --git a/.conflict-side-1/tests/sync/test_lock.py b/.conflict-side-1/tests/sync/test_lock.py deleted file mode 100644 index 42e0b04..0000000 --- a/.conflict-side-1/tests/sync/test_lock.py +++ /dev/null @@ -1,506 +0,0 @@ -"""Test Lock.""" - -import time -from collections.abc import AsyncGenerator - -import pytest -from anyio import WouldBlock, sleep, to_thread -from pytest_mock import MockerFixture - -from grelmicro.sync.abc import SyncBackend -from grelmicro.sync.errors import ( - LockAcquireError, - LockNotOwnedError, - LockReleaseError, - SyncBackendError, -) -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -WORKER_1 = 0 -WORKER_2 = 1 -WORKER_COUNT = 2 - -LOCK_NAME = "test_leased_lock" - - -@pytest.fixture -async def backend() -> AsyncGenerator[SyncBackend]: - """Return Memory Synchronization Backend.""" - async with MemorySyncBackend() as backend: - yield backend - - -@pytest.fixture -def locks(backend: SyncBackend) -> list[Lock]: - """Locks of multiple workers.""" - return [ - Lock( - backend=backend, - name=LOCK_NAME, - worker=f"worker_{i}", - lease_duration=0.01, - retry_interval=0.001, - ) - for i in range(WORKER_COUNT) - ] - - -@pytest.fixture -def lock(locks: list[Lock]) -> Lock: - """Lock.""" - return locks[WORKER_1] - - -async def test_lock_owned(locks: list[Lock]) -> None: - """Test Lock owned.""" - # Act - worker_1_owned_before = await locks[WORKER_1].owned() - worker_2_owned_before = await locks[WORKER_2].owned() - await locks[WORKER_1].acquire() - worker_1_owned_after = await locks[WORKER_1].owned() - worker_2_owned_after = await locks[WORKER_2].owned() - - # Assert - assert worker_1_owned_before is False - assert worker_2_owned_before is False - assert worker_1_owned_after is True - assert worker_2_owned_after is False - - -async def test_lock_from_thread_owned(locks: list[Lock]) -> None: - """Test Lock from thread owned.""" - # Arrange - worker_1_owned_before = None - worker_2_owned_before = None - worker_1_owned_after = None - worker_2_owned_after = None - - # Act - def sync() -> None: - nonlocal worker_1_owned_before - nonlocal worker_2_owned_before - nonlocal worker_1_owned_after - nonlocal worker_2_owned_after - - worker_1_owned_before = locks[WORKER_1].from_thread.owned() - worker_2_owned_before = locks[WORKER_2].from_thread.owned() - locks[WORKER_1].from_thread.acquire() - worker_1_owned_after = locks[WORKER_1].from_thread.owned() - worker_2_owned_after = locks[WORKER_2].from_thread.owned() - - await to_thread.run_sync(sync) - - # Assert - assert worker_1_owned_before is False - assert worker_2_owned_before is False - assert worker_1_owned_after is True - assert worker_2_owned_after is False - - -async def test_lock_context_manager(lock: Lock) -> None: - """Test Lock context manager.""" - # Act - locked_before = await lock.locked() - async with lock: - locked_inside = await lock.locked() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_inside is True - assert locked_after is False - - -async def test_lock_from_thread_context_manager_acquire(lock: Lock) -> None: - """Test Lock from thread context manager.""" - # Arrange - locked_before = None - locked_inside = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_inside - nonlocal locked_after - - locked_before = lock.from_thread.locked() - with lock.from_thread: - locked_inside = lock.from_thread.locked() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_inside is True - assert locked_after is False - - -async def test_lock_context_manager_wait(lock: Lock, locks: list[Lock]) -> None: - """Test Lock context manager wait.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act - locked_before = await lock.locked() - async with locks[WORKER_2]: # Wait until lock expires - locked_inside = await lock.locked() - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_inside is True - assert locked_after is False - - -async def test_lock_from_thread_context_manager_wait( - lock: Lock, locks: list[Lock] -) -> None: - """Test Lock from thread context manager wait.""" - # Arrange - locked_before = None - locked_inside = None - locked_after = None - await locks[WORKER_1].acquire() - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_inside - nonlocal locked_after - - locked_before = lock.from_thread.locked() - with locks[WORKER_2].from_thread: - locked_inside = lock.from_thread.locked() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is True - assert locked_inside is True - assert locked_after is False - - -async def test_lock_acquire(lock: Lock) -> None: - """Test Lock acquire.""" - # Act - locked_before = await lock.locked() - await lock.acquire() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_from_thread_acquire(lock: Lock) -> None: - """Test Lock from thread acquire.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_wait(lock: Lock, locks: list[Lock]) -> None: - """Test Lock acquire wait.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act - locked_before = await lock.locked() - await locks[WORKER_2].acquire() # Wait until lock expires - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_after is True - - -async def test_lock_from_thread_acquire_wait(lock: Lock) -> None: - """Test Lock from thread acquire wait.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_nowait(lock: Lock) -> None: - """Test Lock wait acquire.""" - # Act - locked_before = await lock.locked() - await lock.acquire_nowait() - locked_after = await lock.locked() - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_from_thread_acquire_nowait(lock: Lock) -> None: - """Test Lock from thread wait acquire.""" - # Arrange - locked_before = None - locked_after = None - - # Act - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - locked_before = lock.from_thread.locked() - lock.from_thread.acquire_nowait() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is False - assert locked_after is True - - -async def test_lock_acquire_nowait_would_block(locks: list[Lock]) -> None: - """Test Lock wait acquire would block.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act / Assert - with pytest.raises(WouldBlock): - await locks[WORKER_2].acquire_nowait() - - -async def test_lock_from_thread_acquire_nowait_would_block( - locks: list[Lock], -) -> None: - """Test Lock from thread wait acquire would block.""" - # Arrange - await locks[WORKER_1].acquire() - - # Act / Assert - def sync() -> None: - with pytest.raises(WouldBlock): - locks[WORKER_2].from_thread.acquire_nowait() - - await to_thread.run_sync(sync) - - -async def test_lock_release(lock: Lock) -> None: - """Test Lock release.""" - # Act / Assert - with pytest.raises(LockNotOwnedError): - await lock.release() - - -async def test_lock_from_thread_release(lock: Lock) -> None: - """Test Lock from thread release.""" - - # Act / Assert - def sync() -> None: - with pytest.raises(LockNotOwnedError): - lock.from_thread.release() - - await to_thread.run_sync(sync) - - -async def test_lock_release_acquired(lock: Lock) -> None: - """Test Lock release acquired.""" - # Arrange - await lock.acquire() - - # Act - locked_before = await lock.locked() - await lock.release() - locked_after = await lock.locked() - - # Assert - assert locked_before is True - assert locked_after is False - - -async def test_lock_from_thread_release_acquired(lock: Lock) -> None: - """Test Lock from thread release acquired.""" - # Arrange - locked_before = None - locked_after = None - - def sync() -> None: - nonlocal locked_before - nonlocal locked_after - - lock.from_thread.acquire() - - # Act - locked_before = lock.from_thread.locked() - lock.from_thread.release() - locked_after = lock.from_thread.locked() - - await to_thread.run_sync(sync) - - # Assert - assert locked_before is True - assert locked_after is False - - -async def test_lock_release_expired(locks: list[Lock]) -> None: - """Test Lock release expired.""" - # Arrange - await locks[WORKER_1].acquire() - await sleep(locks[WORKER_1].config.lease_duration) - - # Act - worker_1_locked_before = await locks[WORKER_1].locked() - with pytest.raises(LockNotOwnedError): - await locks[WORKER_2].release() - - # Assert - assert worker_1_locked_before is False - - -async def test_lock_from_thread_release_expired(locks: list[Lock]) -> None: - """Test Lock from thread release expired.""" - # Arrange - worker_1_locked_before = None - - def sync() -> None: - nonlocal worker_1_locked_before - - locks[WORKER_1].from_thread.acquire() - time.sleep(locks[WORKER_1].config.lease_duration) - - # Act - worker_1_locked_before = locks[WORKER_1].from_thread.locked() - with pytest.raises(LockNotOwnedError): - locks[WORKER_2].from_thread.release() - - await to_thread.run_sync(sync) - - # Assert - assert worker_1_locked_before is False - - -async def test_lock_acquire_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock acquire backend error.""" - # Arrange - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Error") - ) - - # Act - with pytest.raises(LockAcquireError): - await lock.acquire() - - -async def test_lock_from_thread_acquire_backend_error( - backend: SyncBackend, - lock: Lock, - mocker: MockerFixture, -) -> None: - """Test Lock from thread acquire backend error.""" - # Arrange - mocker.patch.object( - backend, "acquire", side_effect=Exception("Backend Error") - ) - - # Act - def sync() -> None: - with pytest.raises(LockAcquireError): - lock.from_thread.acquire() - - await to_thread.run_sync(sync) - - -async def test_lock_release_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock release backend error.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Error") - ) - - # Act - await lock.acquire() - with pytest.raises(LockReleaseError): - await lock.release() - - -async def test_lock_from_thread_release_backend_error( - backend: SyncBackend, - lock: Lock, - mocker: MockerFixture, -) -> None: - """Test Lock from thread release backend error.""" - # Arrange - mocker.patch.object( - backend, "release", side_effect=Exception("Backend Error") - ) - - # Act - def sync() -> None: - lock.from_thread.acquire() - with pytest.raises(LockReleaseError): - lock.from_thread.release() - - await to_thread.run_sync(sync) - - -async def test_lock_owned_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock owned backend error.""" - # Arrange - mocker.patch.object( - backend, "owned", side_effect=Exception("Backend Error") - ) - - # Act / Assert - with pytest.raises(SyncBackendError): - await lock.owned() - - -async def test_lock_locked_backend_error( - backend: SyncBackend, lock: Lock, mocker: MockerFixture -) -> None: - """Test Lock locked backend error.""" - # Arrange - mocker.patch.object( - backend, "locked", side_effect=Exception("Backend Error") - ) - - # Act / Assert - with pytest.raises(SyncBackendError): - await lock.locked() diff --git a/.conflict-side-1/tests/sync/test_postgres.py b/.conflict-side-1/tests/sync/test_postgres.py deleted file mode 100644 index ef8dd18..0000000 --- a/.conflict-side-1/tests/sync/test_postgres.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Tests for PostgreSQL Backends.""" - -import pytest - -from grelmicro.errors import OutOfContextError -from grelmicro.sync.errors import SyncSettingsValidationError -from grelmicro.sync.postgres import PostgresSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -URL = "postgres://user:password@localhost:5432/db" - - -@pytest.mark.parametrize( - "table_name", - [ - "locks table", - "%locks", - "locks;table", - "locks' OR '1'='1", - "locks; DROP TABLE users; --", - ], -) -def test_sync_backend_table_name_invalid(table_name: str) -> None: - """Test Synchronization Backend Table Name Invalid.""" - # Act / Assert - with pytest.raises( - ValueError, match="Table name '.*' is not a valid identifier" - ): - PostgresSyncBackend(url=URL, table_name=table_name) - - -async def test_sync_backend_out_of_context_errors() -> None: - """Test Synchronization Backend Out Of Context Errors.""" - # Arrange - backend = PostgresSyncBackend(url=URL) - name = "lock" - key = "token" - - # Act / Assert - with pytest.raises(OutOfContextError): - await backend.acquire(name=name, token=key, duration=1) - with pytest.raises(OutOfContextError): - await backend.release(name=name, token=key) - with pytest.raises(OutOfContextError): - await backend.locked(name=name) - with pytest.raises(OutOfContextError): - await backend.owned(name=name, token=key) - - -@pytest.mark.parametrize( - ("environs"), - [ - { - "POSTGRES_URL": "postgresql://test_user:test_password@test_host:1234/test_db" - }, - { - "POSTGRES_USER": "test_user", - "POSTGRES_PASSWORD": "test_password", - "POSTGRES_HOST": "test_host", - "POSTGRES_PORT": "1234", - "POSTGRES_DB": "test_db", - }, - ], -) -def test_postgres_env_var_settings( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test PostgreSQL Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Act - backend = PostgresSyncBackend() - - # Assert - assert ( - backend._url - == "postgresql://test_user:test_password@test_host:1234/test_db" - ) - - -@pytest.mark.parametrize( - ("environs"), - [ - { - "POSTGRES_URL": "test://test_user:test_password@test_host:1234/test_db" - }, - {"POSTGRES_USER": "test_user"}, - ], -) -def test_postgres_env_var_settings_validation_error( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test PostgreSQL Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Assert / Act - with pytest.raises( - SyncSettingsValidationError, - match=(r"Could not validate environment variables settings:\n"), - ): - PostgresSyncBackend() diff --git a/.conflict-side-1/tests/sync/test_redis.py b/.conflict-side-1/tests/sync/test_redis.py deleted file mode 100644 index a14bad7..0000000 --- a/.conflict-side-1/tests/sync/test_redis.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Tests for Redis Backends.""" - -import pytest - -from grelmicro.sync.errors import SyncSettingsValidationError -from grelmicro.sync.redis import RedisSyncBackend - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] - -URL = "redis://:test_password@test_host:1234/0" - - -@pytest.mark.parametrize( - ("environs"), - [ - {"REDIS_URL": URL}, - { - "REDIS_PASSWORD": "test_password", - "REDIS_HOST": "test_host", - "REDIS_PORT": "1234", - "REDIS_DB": "0", - }, - ], -) -def test_redis_env_var_settings( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Redis Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Act - backend = RedisSyncBackend() - - # Assert - assert backend._url == URL - - -@pytest.mark.parametrize( - ("environs"), - [ - {"REDIS_URL": "test://:test_password@test_host:1234/0"}, - {"REDIS_PASSWORD": "test_password"}, - { - "REDIS_URL": "test://:test_password@test_host:1234/0", - "REDIS_PASSWORD": "test_password", - "REDIS_HOST": "test_host", - "REDIS_PORT": "1234", - "REDIS_DB": "0", - }, - ], -) -def test_redis_env_var_settings_validation_error( - environs: dict[str, str], monkeypatch: pytest.MonkeyPatch -) -> None: - """Test Redis Settings from Environment Variables.""" - # Arrange - for key, value in environs.items(): - monkeypatch.setenv(key, value) - - # Assert / Act - with pytest.raises( - SyncSettingsValidationError, - match=(r"Could not validate environment variables settings:\n"), - ): - RedisSyncBackend() diff --git a/.conflict-side-1/tests/sync/utils.py b/.conflict-side-1/tests/sync/utils.py deleted file mode 100644 index e20356b..0000000 --- a/.conflict-side-1/tests/sync/utils.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Test utilities for Lock.""" - -from anyio import Event, create_task_group, fail_after - -from grelmicro.sync._base import BaseLock - - -async def wait_first_acquired(locks: list[BaseLock]) -> None: - """Wait for the first lock to be acquired.""" - - async def wrapper(lock: BaseLock, event: Event) -> None: - """Send event when lock is acquired.""" - with fail_after(1): - await lock.acquire() - event.set() - - with fail_after(1): - async with create_task_group() as task_group: - event = Event() - for lock in locks: - task_group.start_soon(wrapper, lock, event) - await event.wait() - task_group.cancel_scope.cancel() diff --git a/.conflict-side-1/tests/task/__init__.py b/.conflict-side-1/tests/task/__init__.py deleted file mode 100644 index ebf85b3..0000000 --- a/.conflict-side-1/tests/task/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Grelmicro Task Scheduler Tests.""" diff --git a/.conflict-side-1/tests/task/samples.py b/.conflict-side-1/tests/task/samples.py deleted file mode 100644 index d19c153..0000000 --- a/.conflict-side-1/tests/task/samples.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Test Samples for the Task Component.""" - -from types import TracebackType -from typing import Self - -from anyio import TASK_STATUS_IGNORED, Condition, Event -from anyio.abc import TaskStatus -from typer import echo - -from grelmicro.sync.abc import Synchronization -from grelmicro.task.abc import Task - -condition = Condition() - - -def test1() -> None: - """Test Function.""" - echo("test1") - - -def test2() -> None: - """Test Function.""" - - -def test3(test: str = "test") -> None: - """Test Function.""" - - -async def notify() -> None: - """Test Function that notifies the condition.""" - async with condition: - condition.notify() - - -async def always_fail() -> None: - """Test Function that always fails.""" - msg = "Test Error" - raise ValueError(msg) - - -class SimpleClass: - """Test Class.""" - - def method(self) -> None: - """Test Method.""" - - @staticmethod - def static_method() -> None: - """Test Static Method.""" - - -class EventTask(Task): - """Test Scheduled Task with Event.""" - - def __init__(self, *, event: Event | None = None) -> None: - """Initialize the event task.""" - self._event = event or Event() - - @property - def name(self) -> str: - """Return the task name.""" - return "event_task" - - async def __call__( - self, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED - ) -> None: - """Run the task that sets the event.""" - task_status.started() - self._event.set() - - -class BadLock(Synchronization): - """Bad Lock.""" - - async def __aenter__(self) -> Self: - """Enter the synchronization primitive.""" - msg = "Bad Lock" - raise ValueError(msg) - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> bool | None: - """Exit the synchronization primitive.""" diff --git a/.conflict-side-1/tests/task/test_interval.py b/.conflict-side-1/tests/task/test_interval.py deleted file mode 100644 index 308d456..0000000 --- a/.conflict-side-1/tests/task/test_interval.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Test Interval Task.""" - -import pytest -from anyio import create_task_group, sleep, sleep_forever -from pytest_mock import MockFixture - -from grelmicro.task._interval import IntervalTask -from tests.task.samples import ( - BadLock, - always_fail, - condition, - notify, - test1, -) - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] - -INTERVAL = 0.1 -SLEEP = 0.01 - - -def test_interval_task_init() -> None: - """Test Interval Task Initialization.""" - # Act - task = IntervalTask(interval=1, function=test1) - # Assert - assert task.name == "tests.task.samples:test1" - - -def test_interval_task_init_with_name() -> None: - """Test Interval Task Initialization with Name.""" - # Act - task = IntervalTask(interval=1, function=test1, name="test1") - # Assert - assert task.name == "test1" - - -def test_interval_task_init_with_invalid_interval() -> None: - """Test Interval Task Initialization with Invalid Interval.""" - # Act / Assert - with pytest.raises(ValueError, match="Interval must be greater than 0"): - IntervalTask(interval=0, function=test1) - - -async def test_interval_task_start() -> None: - """Test Interval Task Start.""" - # Arrange - task = IntervalTask(interval=1, function=notify) - # Act - async with create_task_group() as tg: - await tg.start(task) - async with condition: - await condition.wait() - tg.cancel_scope.cancel() - - -async def test_interval_task_execution_error( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test Interval Task Execution Error.""" - # Arrange - task = IntervalTask(interval=1, function=always_fail) - # Act - async with create_task_group() as tg: - await tg.start(task) - await sleep(SLEEP) - tg.cancel_scope.cancel() - - # Assert - assert any( - "Task execution error:" in record.message - for record in caplog.records - if record.levelname == "ERROR" - ) - - -async def test_interval_task_synchronization_error( - caplog: pytest.LogCaptureFixture, -) -> None: - """Test Interval Task Synchronization Error.""" - # Arrange - task = IntervalTask(interval=1, function=notify, sync=BadLock()) - - # Act - async with create_task_group() as tg: - await tg.start(task) - await sleep(SLEEP) - tg.cancel_scope.cancel() - - # Assert - assert any( - "Task synchronization error:" in record.message - for record in caplog.records - if record.levelname == "ERROR" - ) - - -async def test_interval_stop( - caplog: pytest.LogCaptureFixture, mocker: MockFixture -) -> None: - """Test Interval Task stop.""" - # Arrange - caplog.set_level("INFO") - - class CustomBaseException(BaseException): - pass - - mocker.patch( - "grelmicro.task._interval.sleep", side_effect=CustomBaseException - ) - task = IntervalTask(interval=1, function=test1) - - async def leader_election_during_runtime_error() -> None: - async with create_task_group() as tg: - await tg.start(task) - await sleep_forever() - - # Act - with pytest.raises(BaseExceptionGroup): - await leader_election_during_runtime_error() - - # Assert - assert any( - "Task stopped:" in record.message - for record in caplog.records - if record.levelname == "INFO" - ) diff --git a/.conflict-side-1/tests/task/test_manager.py b/.conflict-side-1/tests/task/test_manager.py deleted file mode 100644 index 62c9859..0000000 --- a/.conflict-side-1/tests/task/test_manager.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Test Task Manager.""" - -import pytest -from anyio import Event - -from grelmicro.errors import OutOfContextError -from grelmicro.task import TaskManager -from grelmicro.task.errors import TaskAddOperationError -from tests.task.samples import EventTask - -pytestmark = [pytest.mark.anyio, pytest.mark.timeout(10)] - - -def test_task_manager_init() -> None: - """Test Task Manager Initialization.""" - # Act - task = EventTask() - app = TaskManager() - app_with_tasks = TaskManager(tasks=[task]) - # Assert - assert app.tasks == [] - assert app_with_tasks.tasks == [task] - - -async def test_task_manager_context() -> None: - """Test Task Manager Context.""" - # Arrange - event = Event() - task = EventTask(event=event) - app = TaskManager(tasks=[task]) - - # Act - event_before = event.is_set() - async with app: - event_in_context = event.is_set() - - # Assert - assert event_before is False - assert event_in_context is True - - -@pytest.mark.parametrize("auto_start", [True, False]) -async def test_task_manager_auto_start_disabled(*, auto_start: bool) -> None: - """Test Task Manager Auto Start Disabled.""" - # Arrange - event = Event() - task = EventTask(event=event) - app = TaskManager(auto_start=auto_start, tasks=[task]) - - # Act - event_before = event.is_set() - async with app: - event_in_context = event.is_set() - - # Assert - assert event_before is False - assert event_in_context is auto_start - - -async def test_task_manager_already_started_error() -> None: - """Test Task Manager Already Started Warning.""" - # Arrange - app = TaskManager() - - # Act / Assert - async with app: - with pytest.raises(TaskAddOperationError): - await app.start() - - -async def test_task_manager_out_of_context_errors() -> None: - """Test Task Manager Out of Context Errors.""" - # Arrange - app = TaskManager() - - # Act / Assert - with pytest.raises(OutOfContextError): - await app.start() - - with pytest.raises(OutOfContextError): - await app.__aexit__(None, None, None) diff --git a/.conflict-side-1/tests/task/test_router.py b/.conflict-side-1/tests/task/test_router.py deleted file mode 100644 index ed30af7..0000000 --- a/.conflict-side-1/tests/task/test_router.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Test Task Router.""" - -from functools import partial - -import pytest - -from grelmicro.sync.lock import Lock -from grelmicro.sync.memory import MemorySyncBackend -from grelmicro.task import TaskRouter -from grelmicro.task._interval import IntervalTask -from grelmicro.task.errors import FunctionTypeError, TaskAddOperationError -from tests.task.samples import EventTask, SimpleClass, test1, test2, test3 - - -def test_router_init() -> None: - """Test Task Router Initialization.""" - # Arrange - custom_task = EventTask() - - # Act - router = TaskRouter() - router_with_task = TaskRouter(tasks=[custom_task]) - - # Assert - assert router.tasks == [] - assert router_with_task.tasks == [custom_task] - - -def test_router_add_task() -> None: - """Test Task Router Add Task.""" - # Arrange - custom_task1 = EventTask() - custom_task2 = EventTask() - router = TaskRouter() - router_with_task = TaskRouter(tasks=[custom_task1]) - - # Act - router.add_task(custom_task1) - router_with_task.add_task(custom_task2) - - # Assert - assert router.tasks == [custom_task1] - assert router_with_task.tasks == [custom_task1, custom_task2] - - -def test_router_include_router() -> None: - """Test Task Router Include Router.""" - # Arrange - custom_task1 = EventTask() - custom_task2 = EventTask() - router = TaskRouter(tasks=[custom_task1]) - router_with_task = TaskRouter(tasks=[custom_task2]) - - # Act - router.include_router(router_with_task) - - # Assert - assert router.tasks == [custom_task1, custom_task2] - - -def test_router_interval() -> None: - """Test Task Router add interval task.""" - # Arrange - task_count = 4 - custom_task = EventTask() - router = TaskRouter(tasks=[custom_task]) - sync = Lock(backend=MemorySyncBackend(), name="testlock") - - # Act - router.interval(name="test1", seconds=10, sync=sync)(test1) - router.interval(name="test2", seconds=20)(test2) - router.interval(seconds=10)(test3) - - # Assert - assert len(router.tasks) == task_count - assert ( - sum(isinstance(task, IntervalTask) for task in router.tasks) - == task_count - 1 - ) - assert router.tasks[0].name == "event_task" - assert router.tasks[1].name == "test1" - assert router.tasks[2].name == "test2" - assert router.tasks[3].name == "tests.task.samples:test3" - - -def test_router_interval_name_generation() -> None: - """Test Task Router Interval Name Generation.""" - # Arrange - router = TaskRouter() - - # Act - router.interval(seconds=10)(test1) - router.interval(seconds=10)(SimpleClass.static_method) - router.interval(seconds=10)(SimpleClass.method) - - # Assert - assert router.tasks[0].name == "tests.task.samples:test1" - assert ( - router.tasks[1].name == "tests.task.samples:SimpleClass.static_method" - ) - assert router.tasks[2].name == "tests.task.samples:SimpleClass.method" - - -def test_router_interval_name_generation_error() -> None: - """Test Task Router Interval Name Generation Error.""" - # Arrange - router = TaskRouter() - test_instance = SimpleClass() - - # Act - with pytest.raises(FunctionTypeError, match="nested function"): - - @router.interval(seconds=10) - def nested_function() -> None: - pass - - with pytest.raises(FunctionTypeError, match="lambda"): - router.interval(seconds=10)(lambda _: None) - - with pytest.raises(FunctionTypeError, match="method"): - router.interval(seconds=10)(test_instance.method) - - with pytest.raises(FunctionTypeError, match="partial()"): - router.interval(seconds=10)(partial(test1)) - - with pytest.raises( - FunctionTypeError, - match="callable without __module__ or __qualname__ attribute", - ): - router.interval(seconds=10)(object()) # type: ignore[arg-type] - - -def test_router_add_task_when_started() -> None: - """Test Task Router Add Task When Started.""" - # Arrange - custom_task = EventTask() - router = TaskRouter() - router.do_mark_as_started() - - # Act - with pytest.raises(TaskAddOperationError): - router.add_task(custom_task) - - -def test_router_include_router_when_started() -> None: - """Test Task Router Include Router When Started.""" - # Arrange - router = TaskRouter() - router.do_mark_as_started() - router_child = TaskRouter() - - # Act - with pytest.raises(TaskAddOperationError): - router.include_router(router_child) - - -def test_router_started_propagation() -> None: - """Test Task Router Started Propagation.""" - # Arrange - router = TaskRouter() - router_child = TaskRouter() - router.include_router(router_child) - - # Act - router_started_before = router.started() - router_child_started_before = router_child.started() - router.do_mark_as_started() - router_started_after = router.started() - router_child_started_after = router_child.started() - - # Assert - assert router_started_before is False - assert router_child_started_before is False - assert router_started_after is True - assert router_child_started_after is True diff --git a/.conflict-side-1/uv.lock b/.conflict-side-1/uv.lock deleted file mode 100644 index ff11a2b..0000000 --- a/.conflict-side-1/uv.lock +++ /dev/null @@ -1,1934 +0,0 @@ -version = 1 -requires-python = ">=3.11" -resolution-markers = [ - "python_full_version < '3.13'", - "python_full_version >= '3.13'", -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.6.2.post1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/09/45b9b7a6d4e45c6bcb5bf61d19e3ab87df68e0601fa8c5293de3542546cc/anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c", size = 173422 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/f5/f2b75d2fc6f1a260f340f0e7c6a060f4dd2961cc16884ed851b0d18da06a/anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d", size = 90377 }, -] - -[[package]] -name = "async-timeout" -version = "4.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/d6/21b30a550dafea84b1b8eee21b5e23fa16d010ae006011221f33dcd8d7f8/async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", size = 8345 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/fa/e01228c2938de91d47b307831c62ab9e4001e747789d0b05baf779a6488c/async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028", size = 5721 }, -] - -[[package]] -name = "asyncpg" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/0e/f5d708add0d0b97446c402db7e8dd4c4183c13edaabe8a8500b411e7b495/asyncpg-0.30.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5e0511ad3dec5f6b4f7a9e063591d407eee66b88c14e2ea636f187da1dcfff6a", size = 674506 }, - { url = "https://files.pythonhosted.org/packages/6a/a0/67ec9a75cb24a1d99f97b8437c8d56da40e6f6bd23b04e2f4ea5d5ad82ac/asyncpg-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:915aeb9f79316b43c3207363af12d0e6fd10776641a7de8a01212afd95bdf0ed", size = 645922 }, - { url = "https://files.pythonhosted.org/packages/5c/d9/a7584f24174bd86ff1053b14bb841f9e714380c672f61c906eb01d8ec433/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c198a00cce9506fcd0bf219a799f38ac7a237745e1d27f0e1f66d3707c84a5a", size = 3079565 }, - { url = "https://files.pythonhosted.org/packages/a0/d7/a4c0f9660e333114bdb04d1a9ac70db690dd4ae003f34f691139a5cbdae3/asyncpg-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3326e6d7381799e9735ca2ec9fd7be4d5fef5dcbc3cb555d8a463d8460607956", size = 3109962 }, - { url = "https://files.pythonhosted.org/packages/3c/21/199fd16b5a981b1575923cbb5d9cf916fdc936b377e0423099f209e7e73d/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:51da377487e249e35bd0859661f6ee2b81db11ad1f4fc036194bc9cb2ead5056", size = 3064791 }, - { url = "https://files.pythonhosted.org/packages/77/52/0004809b3427534a0c9139c08c87b515f1c77a8376a50ae29f001e53962f/asyncpg-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc6d84136f9c4d24d358f3b02be4b6ba358abd09f80737d1ac7c444f36108454", size = 3188696 }, - { url = "https://files.pythonhosted.org/packages/52/cb/fbad941cd466117be58b774a3f1cc9ecc659af625f028b163b1e646a55fe/asyncpg-0.30.0-cp311-cp311-win32.whl", hash = "sha256:574156480df14f64c2d76450a3f3aaaf26105869cad3865041156b38459e935d", size = 567358 }, - { url = "https://files.pythonhosted.org/packages/3c/0a/0a32307cf166d50e1ad120d9b81a33a948a1a5463ebfa5a96cc5606c0863/asyncpg-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:3356637f0bd830407b5597317b3cb3571387ae52ddc3bca6233682be88bbbc1f", size = 629375 }, - { url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 }, - { url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 }, - { url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 }, - { url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 }, - { url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 }, - { url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 }, - { url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 }, - { url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 }, - { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 }, - { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 }, - { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 }, - { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 }, - { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 }, - { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 }, - { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 }, - { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 }, -] - -[[package]] -name = "babel" -version = "2.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/74/f1bc80f23eeba13393b7222b11d95ca3af2c1e28edca18af487137eefed9/babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316", size = 9348104 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/20/bc79bc575ba2e2a7f70e8a1155618bb1301eaa5132a8271373a6903f73f8/babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b", size = 9587599 }, -] - -[[package]] -name = "backports-tarfile" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181 }, -] - -[[package]] -name = "certifi" -version = "2024.8.30" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, -] - -[[package]] -name = "cffi" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, - { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, - { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, - { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, - { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, - { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, - { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, - { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, - { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, - { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, - { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, - { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, - { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, - { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, - { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, - { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, - { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, - { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, - { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, - { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, - { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, - { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, - { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, - { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, - { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, - { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, - { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, - { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, - { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, - { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, - { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, - { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, - { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, - { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, - { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, - { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, - { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, - { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, - { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, - { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, - { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, - { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, - { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, - { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, - { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, - { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, - { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, - { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, - { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, - { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, - { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, - { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, - { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, - { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, - { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, - { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, - { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, - { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, - { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, - { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, - { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, - { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, - { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, - { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, - { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, - { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, - { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, - { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, - { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, - { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, - { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, - { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, - { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, - { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, - { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, - { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, -] - -[[package]] -name = "click" -version = "8.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "coverage" -version = "7.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/12/3669b6382792783e92046730ad3327f53b2726f0603f4c311c4da4824222/coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73", size = 798716 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/31/9c0cf84f0dfcbe4215b7eb95c31777cdc0483c13390e69584c8150c85175/coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b", size = 206819 }, - { url = "https://files.pythonhosted.org/packages/53/ed/a38401079ad320ad6e054a01ec2b61d270511aeb3c201c80e99c841229d5/coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25", size = 207263 }, - { url = "https://files.pythonhosted.org/packages/20/e7/c3ad33b179ab4213f0d70da25a9c214d52464efa11caeab438592eb1d837/coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546", size = 239205 }, - { url = "https://files.pythonhosted.org/packages/36/91/fc02e8d8e694f557752120487fd982f654ba1421bbaa5560debf96ddceda/coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b", size = 236612 }, - { url = "https://files.pythonhosted.org/packages/cc/57/cb08f0eda0389a9a8aaa4fc1f9fec7ac361c3e2d68efd5890d7042c18aa3/coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e", size = 238479 }, - { url = "https://files.pythonhosted.org/packages/d5/c9/2c7681a9b3ca6e6f43d489c2e6653a53278ed857fd6e7010490c307b0a47/coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718", size = 237405 }, - { url = "https://files.pythonhosted.org/packages/b5/4e/ebfc6944b96317df8b537ae875d2e57c27b84eb98820bc0a1055f358f056/coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db", size = 236038 }, - { url = "https://files.pythonhosted.org/packages/13/f2/3a0bf1841a97c0654905e2ef531170f02c89fad2555879db8fe41a097871/coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522", size = 236812 }, - { url = "https://files.pythonhosted.org/packages/b9/9c/66bf59226b52ce6ed9541b02d33e80a6e816a832558fbdc1111a7bd3abd4/coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf", size = 209400 }, - { url = "https://files.pythonhosted.org/packages/2a/a0/b0790934c04dfc8d658d4a62acb8f7ca0efdf3818456fcad757b11c6479d/coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19", size = 210243 }, - { url = "https://files.pythonhosted.org/packages/7d/e7/9291de916d084f41adddfd4b82246e68d61d6a75747f075f7e64628998d2/coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2", size = 207013 }, - { url = "https://files.pythonhosted.org/packages/27/03/932c2c5717a7fa80cd43c6a07d3177076d97b79f12f40f882f9916db0063/coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117", size = 207251 }, - { url = "https://files.pythonhosted.org/packages/d5/3f/0af47dcb9327f65a45455fbca846fe96eb57c153af46c4754a3ba678938a/coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613", size = 240268 }, - { url = "https://files.pythonhosted.org/packages/8a/3c/37a9d81bbd4b23bc7d46ca820e16174c613579c66342faa390a271d2e18b/coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27", size = 237298 }, - { url = "https://files.pythonhosted.org/packages/c0/70/6b0627e5bd68204ee580126ed3513140b2298995c1233bd67404b4e44d0e/coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52", size = 239367 }, - { url = "https://files.pythonhosted.org/packages/3c/eb/634d7dfab24ac3b790bebaf9da0f4a5352cbc125ce6a9d5c6cf4c6cae3c7/coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2", size = 238853 }, - { url = "https://files.pythonhosted.org/packages/d9/0d/8e3ed00f1266ef7472a4e33458f42e39492e01a64281084fb3043553d3f1/coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1", size = 237160 }, - { url = "https://files.pythonhosted.org/packages/ce/9c/4337f468ef0ab7a2e0887a9c9da0e58e2eada6fc6cbee637a4acd5dfd8a9/coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5", size = 238824 }, - { url = "https://files.pythonhosted.org/packages/5e/09/3e94912b8dd37251377bb02727a33a67ee96b84bbbe092f132b401ca5dd9/coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17", size = 209639 }, - { url = "https://files.pythonhosted.org/packages/01/69/d4f3a4101171f32bc5b3caec8ff94c2c60f700107a6aaef7244b2c166793/coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08", size = 210428 }, - { url = "https://files.pythonhosted.org/packages/c2/4d/2dede4f7cb5a70fb0bb40a57627fddf1dbdc6b9c1db81f7c4dcdcb19e2f4/coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9", size = 207039 }, - { url = "https://files.pythonhosted.org/packages/3f/f9/d86368ae8c79e28f1fb458ebc76ae9ff3e8bd8069adc24e8f2fed03c58b7/coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba", size = 207298 }, - { url = "https://files.pythonhosted.org/packages/64/c5/b4cc3c3f64622c58fbfd4d8b9a7a8ce9d355f172f91fcabbba1f026852f6/coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c", size = 239813 }, - { url = "https://files.pythonhosted.org/packages/8a/86/14c42e60b70a79b26099e4d289ccdfefbc68624d096f4481163085aa614c/coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06", size = 236959 }, - { url = "https://files.pythonhosted.org/packages/7f/f8/4436a643631a2fbab4b44d54f515028f6099bfb1cd95b13cfbf701e7f2f2/coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f", size = 238950 }, - { url = "https://files.pythonhosted.org/packages/49/50/1571810ddd01f99a0a8be464a4ac8b147f322cd1e8e296a1528984fc560b/coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b", size = 238610 }, - { url = "https://files.pythonhosted.org/packages/f3/8c/6312d241fe7cbd1f0cade34a62fea6f333d1a261255d76b9a87074d8703c/coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21", size = 236697 }, - { url = "https://files.pythonhosted.org/packages/ce/5f/fef33dfd05d87ee9030f614c857deb6df6556b8f6a1c51bbbb41e24ee5ac/coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a", size = 238541 }, - { url = "https://files.pythonhosted.org/packages/a9/64/6a984b6e92e1ea1353b7ffa08e27f707a5e29b044622445859200f541e8c/coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e", size = 209707 }, - { url = "https://files.pythonhosted.org/packages/5c/60/ce5a9e942e9543783b3db5d942e0578b391c25cdd5e7f342d854ea83d6b7/coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963", size = 210439 }, - { url = "https://files.pythonhosted.org/packages/78/53/6719677e92c308207e7f10561a1b16ab8b5c00e9328efc9af7cfd6fb703e/coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f", size = 207784 }, - { url = "https://files.pythonhosted.org/packages/fa/dd/7054928930671fcb39ae6a83bb71d9ab5f0afb733172543ced4b09a115ca/coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806", size = 208058 }, - { url = "https://files.pythonhosted.org/packages/b5/7d/fd656ddc2b38301927b9eb3aae3fe827e7aa82e691923ed43721fd9423c9/coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11", size = 250772 }, - { url = "https://files.pythonhosted.org/packages/90/d0/eb9a3cc2100b83064bb086f18aedde3afffd7de6ead28f69736c00b7f302/coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3", size = 246490 }, - { url = "https://files.pythonhosted.org/packages/45/44/3f64f38f6faab8a0cfd2c6bc6eb4c6daead246b97cf5f8fc23bf3788f841/coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a", size = 248848 }, - { url = "https://files.pythonhosted.org/packages/5d/11/4c465a5f98656821e499f4b4619929bd5a34639c466021740ecdca42aa30/coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc", size = 248340 }, - { url = "https://files.pythonhosted.org/packages/f1/96/ebecda2d016cce9da812f404f720ca5df83c6b29f65dc80d2000d0078741/coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70", size = 246229 }, - { url = "https://files.pythonhosted.org/packages/16/d9/3d820c00066ae55d69e6d0eae11d6149a5ca7546de469ba9d597f01bf2d7/coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef", size = 247510 }, - { url = "https://files.pythonhosted.org/packages/8f/c3/4fa1eb412bb288ff6bfcc163c11700ff06e02c5fad8513817186e460ed43/coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e", size = 210353 }, - { url = "https://files.pythonhosted.org/packages/7e/77/03fc2979d1538884d921c2013075917fc927f41cd8526909852fe4494112/coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1", size = 211502 }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[package]] -name = "cryptography" -version = "43.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/05/07b55d1fa21ac18c3a8c79f764e2514e6f6a9698f1be44994f5adf0d29db/cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", size = 686989 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/01/4896f3d1b392025d4fcbecf40fdea92d3df8662123f6835d0af828d148fd/cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", size = 3760905 }, - { url = "https://files.pythonhosted.org/packages/0a/be/f9a1f673f0ed4b7f6c643164e513dbad28dd4f2dcdf5715004f172ef24b6/cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", size = 3977271 }, - { url = "https://files.pythonhosted.org/packages/4e/49/80c3a7b5514d1b416d7350830e8c422a4d667b6d9b16a9392ebfd4a5388a/cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", size = 3746606 }, - { url = "https://files.pythonhosted.org/packages/0e/16/a28ddf78ac6e7e3f25ebcef69ab15c2c6be5ff9743dd0709a69a4f968472/cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", size = 3986484 }, - { url = "https://files.pythonhosted.org/packages/01/f5/69ae8da70c19864a32b0315049866c4d411cce423ec169993d0434218762/cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", size = 3852131 }, - { url = "https://files.pythonhosted.org/packages/fd/db/e74911d95c040f9afd3612b1f732e52b3e517cb80de8bf183be0b7d413c6/cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", size = 4075647 }, - { url = "https://files.pythonhosted.org/packages/2f/78/55356eb9075d0be6e81b59f45c7b48df87f76a20e73893872170471f3ee8/cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", size = 3762968 }, - { url = "https://files.pythonhosted.org/packages/2a/2c/488776a3dc843f95f86d2f957ca0fc3407d0242b50bede7fad1e339be03f/cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", size = 3977754 }, - { url = "https://files.pythonhosted.org/packages/7c/04/2345ca92f7a22f601a9c62961741ef7dd0127c39f7310dffa0041c80f16f/cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7", size = 3749458 }, - { url = "https://files.pythonhosted.org/packages/ac/25/e715fa0bc24ac2114ed69da33adf451a38abb6f3f24ec207908112e9ba53/cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", size = 3988220 }, - { url = "https://files.pythonhosted.org/packages/21/ce/b9c9ff56c7164d8e2edfb6c9305045fbc0df4508ccfdb13ee66eb8c95b0e/cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", size = 3853898 }, - { url = "https://files.pythonhosted.org/packages/2a/33/b3682992ab2e9476b9c81fff22f02c8b0a1e6e1d49ee1750a67d85fd7ed2/cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", size = 4076592 }, -] - -[[package]] -name = "cyclic" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bf/9f/becc4fea44301f232e4eba17752001bd708e3c042fef37a72b9af7ddf4b5/cyclic-1.0.0.tar.gz", hash = "sha256:ecddd56cb831ee3e6b79f61ecb0ad71caee606c507136867782911aa01c3e5eb", size = 2167 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/c0/9f59d2ebd9d585e1681c51767eb138bcd9d0ea770f6fc003cd875c7f5e62/cyclic-1.0.0-py3-none-any.whl", hash = "sha256:32d8181d7698f426bce6f14f4c3921ef95b6a84af9f96192b59beb05bc00c3ed", size = 2547 }, -] - -[[package]] -name = "distlib" -version = "0.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, -] - -[[package]] -name = "docker" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "requests" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/26/57c6fb270950d476074c087527a558ccb6f4436657314bfb6cdf484114c4/docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0", size = 147774 }, -] - -[[package]] -name = "fast-depends" -version = "2.4.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/f5/8b42b7588a67ad78991e5e7ca0e0c6a1ded535a69a725e4e48d3346a20c1/fast_depends-2.4.12.tar.gz", hash = "sha256:9393e6de827f7afa0141e54fa9553b737396aaf06bd0040e159d1f790487b16d", size = 16682 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/08/4adb160d8394053289fdf3b276e93b53271fd463e54fff8911b23c1db4ed/fast_depends-2.4.12-py3-none-any.whl", hash = "sha256:9e5d110ddc962329e46c9b35e5fe65655984247a13ee3ca5a33186db7d2d75c2", size = 17651 }, -] - -[[package]] -name = "fastapi" -version = "0.115.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/29/f71316b9273b6552a263748e49cd7b83898dc9499a663d30c7b9cb853cb8/fastapi-0.115.5.tar.gz", hash = "sha256:0e7a4d0dc0d01c68df21887cce0945e72d3c48b9f4f79dfe7a7d53aa08fbb289", size = 301047 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/c4/148d5046a96c428464557264877ae5a9338a83bbe0df045088749ec89820/fastapi-0.115.5-py3-none-any.whl", hash = "sha256:596b95adbe1474da47049e802f9a65ab2ffa9c2b07e7efee70eb8a66c9f2f796", size = 94866 }, -] - -[[package]] -name = "fastapi-cli" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typer" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/f8/1ad5ce32d029aeb9117e9a5a9b3e314a8477525d60c12a9b7730a3c186ec/fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f", size = 15571 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/ea/4b5011012ac925fe2f83b19d0e09cee9d324141ec7bf5e78bb2817f96513/fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46", size = 9489 }, -] - -[[package]] -name = "faststream" -version = "0.5.30" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "fast-depends" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/d3/c2a3e1233274c93a4978cbac210a81ba05cee09e2e0051049b40f55406f1/faststream-0.5.30.tar.gz", hash = "sha256:50ad5288719cfa75c13e9c277d40afae62533a590facad6e6d215e868f2b97f4", size = 284478 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/ce/a9eec6c2c9803de6bc2b2a5cac35d56b8908c64fcdd4c73616c1a16c9b90/faststream-0.5.30-py3-none-any.whl", hash = "sha256:bf48826be99210f3e9c7dff1b2a17b4bc4762c873c5558ac81b9b873549ae6a1", size = 382011 }, -] - -[[package]] -name = "filelock" -version = "3.16.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 }, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, -] - -[[package]] -name = "grelmicro" -version = "0.2.2" -source = { editable = "." } -dependencies = [ - { name = "anyio" }, - { name = "fast-depends" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, -] - -[package.optional-dependencies] -postgres = [ - { name = "asyncpg" }, -] -redis = [ - { name = "redis" }, -] -standard = [ - { name = "loguru" }, - { name = "orjson" }, -] - -[package.dev-dependencies] -dev = [ - { name = "fastapi" }, - { name = "fastapi-cli" }, - { name = "faststream" }, - { name = "hatch" }, - { name = "mdx-include" }, - { name = "mypy" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "pytest-mock" }, - { name = "pytest-randomly" }, - { name = "pytest-timeout" }, - { name = "ruff" }, - { name = "testcontainers", extra = ["redis"] }, -] -docs = [ - { name = "mkdocs-material" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, -] - -[package.metadata] -requires-dist = [ - { name = "anyio", specifier = ">=4.0.0" }, - { name = "asyncpg", marker = "extra == 'postgres'", specifier = ">=0.30.0" }, - { name = "fast-depends", specifier = ">=2.0.0" }, - { name = "loguru", marker = "extra == 'standard'", specifier = ">=0.7.2" }, - { name = "orjson", marker = "extra == 'standard'", specifier = ">=3.10.11" }, - { name = "pydantic", specifier = ">=2.5.0" }, - { name = "pydantic-settings", specifier = ">=2.5.0" }, - { name = "redis", marker = "extra == 'redis'", specifier = ">=5.0.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "fastapi", specifier = ">=0.115.5" }, - { name = "fastapi-cli", specifier = ">=0.0.5" }, - { name = "faststream", specifier = ">=0.5.30" }, - { name = "hatch", specifier = ">=1.13.0" }, - { name = "mdx-include", specifier = ">=1.4.2" }, - { name = "mypy", specifier = ">=1.12.0" }, - { name = "pre-commit", specifier = ">=4.0.1" }, - { name = "pytest", specifier = ">=8.0.0" }, - { name = "pytest-cov", specifier = ">=6.0.0" }, - { name = "pytest-mock", specifier = ">=3.14.0" }, - { name = "pytest-randomly", specifier = ">=3.16.0" }, - { name = "pytest-timeout", specifier = ">=2.3.1" }, - { name = "ruff", specifier = ">=0.7.4" }, - { name = "testcontainers", extras = ["postgres", "redis"], specifier = ">=4.8.2" }, -] -docs = [ - { name = "mkdocs-material", specifier = ">=9.5.44" }, - { name = "pygments", specifier = ">=2.18.0" }, - { name = "pymdown-extensions", specifier = ">=10.12" }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, -] - -[[package]] -name = "hatch" -version = "1.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "hatchling" }, - { name = "httpx" }, - { name = "hyperlink" }, - { name = "keyring" }, - { name = "packaging" }, - { name = "pexpect" }, - { name = "platformdirs" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "tomli-w" }, - { name = "tomlkit" }, - { name = "userpath" }, - { name = "uv" }, - { name = "virtualenv" }, - { name = "zstandard" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/ed/5001de278f8d7381cbc84f5efdae72308fe37493bc063878f6a1ac07dab8/hatch-1.13.0.tar.gz", hash = "sha256:5e1a75770cfe8f3ebae3abfded3a976238b0acefd19cdabc5245597525b8066f", size = 5188060 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/8d/6d965a22bc38cec091ba82131624bb5d75471094d7fe05e829536de3de2f/hatch-1.13.0-py3-none-any.whl", hash = "sha256:bb1a18558a626279cae338b4d8a9d3ca4226d5e06d50de600608c57acd131b67", size = 125757 }, -] - -[[package]] -name = "hatchling" -version = "1.26.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "pathspec" }, - { name = "pluggy" }, - { name = "trove-classifiers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/47/7ec270a9567262ae3cb32dd420d2b53bf7aee769aca1f240eae0426b5bbc/hatchling-1.26.3.tar.gz", hash = "sha256:b672a9c36a601a06c4e88a1abb1330639ee8e721e0535a37536e546a667efc7a", size = 54968 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/41/b3e29dc4fe623794070e5dfbb9915acb649ce05d6472f005470cbed9de83/hatchling-1.26.3-py3-none-any.whl", hash = "sha256:c407e1c6c17b574584a66ae60e8e9a01235ecb6dc61d01559bb936577aaf5846", size = 75773 }, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, -] - -[[package]] -name = "httptools" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029 }, - { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492 }, - { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891 }, - { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788 }, - { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120 }, - { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565 }, - { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 }, - { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 }, - { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 }, - { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 }, - { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 }, - { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 }, - { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 }, - { url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 }, - { url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 }, - { url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 }, - { url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 }, - { url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 }, - { url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 }, - { url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 }, -] - -[[package]] -name = "httpx" -version = "0.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, - { name = "sniffio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, -] - -[[package]] -name = "hyperlink" -version = "21.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, -] - -[[package]] -name = "identify" -version = "2.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/79/7a520fc5011e02ca3f3285b5f6820eaf80443eb73e3733f73c02fb42ba0b/identify-2.6.2.tar.gz", hash = "sha256:fab5c716c24d7a789775228823797296a2994b075fb6080ac83a102772a98cbd", size = 99113 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/86/c4395700f3c5475424fb5c41e20c16be28d10c904aee4d005ba3217fc8e7/identify-2.6.2-py2.py3-none-any.whl", hash = "sha256:c097384259f49e372f4ea00a19719d95ae27dd5ff0fd77ad630aa891306b82f3", size = 98982 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "importlib-metadata" -version = "8.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, -] - -[[package]] -name = "jaraco-classes" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777 }, -] - -[[package]] -name = "jaraco-context" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825 }, -] - -[[package]] -name = "jaraco-functools" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187 }, -] - -[[package]] -name = "jeepney" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/154cf374c2daf2020e05c3c6a03c91348d59b23c5366e968feb198306fdf/jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806", size = 106005 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/72/2a1e2290f1ab1e06f71f3d0f1646c9e4634e70e1d37491535e19266e8dc9/jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755", size = 48435 }, -] - -[[package]] -name = "jinja2" -version = "3.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, -] - -[[package]] -name = "keyring" -version = "25.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, - { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, - { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, - { name = "secretstorage", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/24/64447b13df6a0e2797b586dad715766d756c932ce8ace7f67bd384d76ae0/keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6", size = 62675 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/c9/353c156fa2f057e669106e5d6bcdecf85ef8d3536ce68ca96f18dc7b6d6f/keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741", size = 39096 }, -] - -[[package]] -name = "loguru" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "win32-setctime", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/30/d87a423766b24db416a46e9335b9602b054a72b96a88a241f2b09b560fa8/loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac", size = 145103 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/0a/4f6fed21aa246c6b49b561ca55facacc2a44b87d65b8b92362a8e99ba202/loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb", size = 62549 }, -] - -[[package]] -name = "markdown" -version = "3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 }, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, -] - -[[package]] -name = "mdx-include" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cyclic" }, - { name = "markdown" }, - { name = "rcslice" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/f0/f395a9cf164471d3c7bbe58cbd64d74289575a8b85a962b49a804ab7ed34/mdx_include-1.4.2.tar.gz", hash = "sha256:992f9fbc492b5cf43f7d8cb4b90b52a4e4c5fdd7fd04570290a83eea5c84f297", size = 15051 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/40/6844997dee251103c5a4c4eb0d1d2f2162b7c29ffc4e86de3cd68d269be2/mdx_include-1.4.2-py3-none-any.whl", hash = "sha256:cfbeadd59985f27a9b70cb7ab0a3d209892fe1bb1aa342df055e0b135b3c9f34", size = 11591 }, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, -] - -[[package]] -name = "mkdocs-material" -version = "9.5.44" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f7/56/182d8121db9ab553cdf9bc58d5972b89833f60b63272f693c1f2b849b640/mkdocs_material-9.5.44.tar.gz", hash = "sha256:f3a6c968e524166b3f3ed1fb97d3ed3e0091183b0545cedf7156a2a6804c56c0", size = 3964306 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/eb/a801d00e0e210d82184aacce596906ec065422c78a7319244ba0771c4ded/mkdocs_material-9.5.44-py3-none-any.whl", hash = "sha256:47015f9c167d58a5ff5e682da37441fc4d66a1c79334bfc08d774763cacf69ca", size = 8674509 }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, -] - -[[package]] -name = "more-itertools" -version = "10.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/78/65922308c4248e0eb08ebcbe67c95d48615cc6f27854b6f2e57143e9178f/more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6", size = 121020 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/7e/3a64597054a70f7c86eb0a7d4fc315b8c1ab932f64883a297bdffeb5f967/more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", size = 60952 }, -] - -[[package]] -name = "mypy" -version = "1.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/03/744330105a74dc004578f47ec27e1bf66b1dd5664ea444d18423e41343bd/mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d", size = 3150767 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/0a/70de7c97a86cb85535077ab5cef1cbc4e2812fd2e9cc21d78eb561a6b80f/mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", size = 10940998 }, - { url = "https://files.pythonhosted.org/packages/c0/97/9ed6d4834d7549936ab88533b302184fb568a0940c4000d2aaee6dc07112/mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", size = 10108523 }, - { url = "https://files.pythonhosted.org/packages/48/41/1686f37d09c915dfc5b683e20cc99dabac199900b5ca6d22747b99ddcb50/mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", size = 12505553 }, - { url = "https://files.pythonhosted.org/packages/8d/2b/2dbcaa7e97b23f27ced77493256ee878f4a140ac750e198630ff1b9b60c6/mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", size = 12988634 }, - { url = "https://files.pythonhosted.org/packages/54/55/710d082e91a2ccaea21214229b11f9215a9d22446f949491b5457655e82b/mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", size = 9630747 }, - { url = "https://files.pythonhosted.org/packages/8a/74/b9e0e4f06e951e277058f878302faa154d282ca11274c59fe08353f52949/mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", size = 11079902 }, - { url = "https://files.pythonhosted.org/packages/9f/62/fcad290769db3eb0de265094cef5c94d6075c70bc1e42b67eee4ca192dcc/mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", size = 10072373 }, - { url = "https://files.pythonhosted.org/packages/cb/27/9ac78349c2952e4446288ec1174675ab9e0160ed18c2cb1154fa456c54e8/mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", size = 12589779 }, - { url = "https://files.pythonhosted.org/packages/7c/4a/58cebd122cf1cba95680ac51303fbeb508392413ca64e3e711aa7d4877aa/mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", size = 13044459 }, - { url = "https://files.pythonhosted.org/packages/5b/c7/672935e2a3f9bcc07b1b870395a653f665657bef3cdaa504ad99f56eadf0/mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", size = 9731919 }, - { url = "https://files.pythonhosted.org/packages/bb/b0/092be5094840a401940c95224f63bb2a8f09bce9251ac1df180ec523830c/mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", size = 11068611 }, - { url = "https://files.pythonhosted.org/packages/9a/86/f20f53b8f062876c39602243d7a59b5cabd6b24315d8de511d607fa4de6a/mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", size = 10068036 }, - { url = "https://files.pythonhosted.org/packages/84/c7/1dbd6575785522da1d4c1ac2c419505fcf23bee74811880cac447a4a77ab/mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", size = 12585671 }, - { url = "https://files.pythonhosted.org/packages/46/8a/f6ae18b446eb2bccce54c4bd94065bcfe417d6c67021dcc032bf1e720aff/mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", size = 13036083 }, - { url = "https://files.pythonhosted.org/packages/59/e6/fc65fde3dc7156fce8d49ba21c7b1f5d866ad50467bf196ca94a7f6d2c9e/mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", size = 9735467 }, - { url = "https://files.pythonhosted.org/packages/84/6b/1db9de4e0764778251fb2d64cb7455cf6db75dc99c9f72c8b7e74b6a8a17/mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", size = 2646060 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, -] - -[[package]] -name = "orjson" -version = "3.10.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/3a/10320029954badc7eaa338a15ee279043436f396e965dafc169610e4933f/orjson-3.10.11.tar.gz", hash = "sha256:e35b6d730de6384d5b2dab5fd23f0d76fae8bbc8c353c2f78210aa5fa4beb3ef", size = 5444879 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/25/c869a1fbd481dcb02c70032fd6a7243de7582bc48c7cae03d6f0985a11c0/orjson-3.10.11-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1444f9cb7c14055d595de1036f74ecd6ce15f04a715e73f33bb6326c9cef01b6", size = 266432 }, - { url = "https://files.pythonhosted.org/packages/6a/a4/2307155ee92457d28345308f7d8c0e712348404723025613adeffcb531d0/orjson-3.10.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdec57fe3b4bdebcc08a946db3365630332dbe575125ff3d80a3272ebd0ddafe", size = 151884 }, - { url = "https://files.pythonhosted.org/packages/aa/82/daf1b2596dd49fe44a1bd92367568faf6966dcb5d7f99fd437c3d0dc2de6/orjson-3.10.11-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eed32f33a0ea6ef36ccc1d37f8d17f28a1d6e8eefae5928f76aff8f1df85e67", size = 167371 }, - { url = "https://files.pythonhosted.org/packages/63/a8/680578e4589be5fdcfe0186bdd7dc6fe4a39d30e293a9da833cbedd5a56e/orjson-3.10.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80df27dd8697242b904f4ea54820e2d98d3f51f91e97e358fc13359721233e4b", size = 154368 }, - { url = "https://files.pythonhosted.org/packages/6e/ce/9cb394b5b01ef34579eeca6d704b21f97248f607067ce95a24ba9ea2698e/orjson-3.10.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:705f03cee0cb797256d54de6695ef219e5bc8c8120b6654dd460848d57a9af3d", size = 165725 }, - { url = "https://files.pythonhosted.org/packages/49/24/55eeb05cfb36b9e950d05743e6f6fdb7d5f33ca951a27b06ea6d03371aed/orjson-3.10.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03246774131701de8e7059b2e382597da43144a9a7400f178b2a32feafc54bd5", size = 142522 }, - { url = "https://files.pythonhosted.org/packages/94/0c/3a6a289e56dcc9fe67dc6b6d33c91dc5491f9ec4a03745efd739d2acf0ff/orjson-3.10.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8b5759063a6c940a69c728ea70d7c33583991c6982915a839c8da5f957e0103a", size = 146934 }, - { url = "https://files.pythonhosted.org/packages/1d/5c/a08c0e90a91e2526029a4681ff8c6fc4495b8bab77d48801144e378c7da9/orjson-3.10.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:677f23e32491520eebb19c99bb34675daf5410c449c13416f7f0d93e2cf5f981", size = 142904 }, - { url = "https://files.pythonhosted.org/packages/2c/c9/710286a60b14e88288ca014d43befb08bb0a4a6a0f51b875f8c2f05e8205/orjson-3.10.11-cp311-none-win32.whl", hash = "sha256:a11225d7b30468dcb099498296ffac36b4673a8398ca30fdaec1e6c20df6aa55", size = 144459 }, - { url = "https://files.pythonhosted.org/packages/7d/68/ef7b920e0a09e02b1a30daca1b4864938463797995c2fabe457c1500220a/orjson-3.10.11-cp311-none-win_amd64.whl", hash = "sha256:df8c677df2f9f385fcc85ab859704045fa88d4668bc9991a527c86e710392bec", size = 136444 }, - { url = "https://files.pythonhosted.org/packages/78/f2/a712dbcef6d84ff53e13056e7dc69d9d4844bd1e35e51b7431679ddd154d/orjson-3.10.11-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:360a4e2c0943da7c21505e47cf6bd725588962ff1d739b99b14e2f7f3545ba51", size = 266505 }, - { url = "https://files.pythonhosted.org/packages/94/54/53970831786d71f98fdc13c0f80451324c9b5c20fbf42f42ef6147607ee7/orjson-3.10.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:496e2cb45de21c369079ef2d662670a4892c81573bcc143c4205cae98282ba97", size = 151745 }, - { url = "https://files.pythonhosted.org/packages/35/38/482667da1ca7ef95d44d4d2328257a144fd2752383e688637c53ed474d2a/orjson-3.10.11-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7dfa8db55c9792d53c5952900c6a919cfa377b4f4534c7a786484a6a4a350c19", size = 167274 }, - { url = "https://files.pythonhosted.org/packages/23/2f/5bb0a03e819781d82dadb733fde8ebbe20d1777d1a33715d45ada4d82ce8/orjson-3.10.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51f3382415747e0dbda9dade6f1e1a01a9d37f630d8c9049a8ed0e385b7a90c0", size = 154605 }, - { url = "https://files.pythonhosted.org/packages/49/e9/14cc34d45c7bd51665aff9b1bb6b83475a61c52edb0d753fffe1adc97764/orjson-3.10.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f35a1b9f50a219f470e0e497ca30b285c9f34948d3c8160d5ad3a755d9299433", size = 165874 }, - { url = "https://files.pythonhosted.org/packages/7b/61/c2781ecf90f99623e97c67a31e8553f38a1ecebaf3189485726ac8641576/orjson-3.10.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f3b7c5803138e67028dde33450e054c87e0703afbe730c105f1fcd873496d5", size = 142813 }, - { url = "https://files.pythonhosted.org/packages/4d/4f/18c83f78b501b6608569b1610fcb5a25c9bb9ab6a7eb4b3a55131e0fba37/orjson-3.10.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f91d9eb554310472bd09f5347950b24442600594c2edc1421403d7610a0998fd", size = 146762 }, - { url = "https://files.pythonhosted.org/packages/ba/19/ea80d5b575abd3f76a790409c2b7b8a60f3fc9447965c27d09613b8bddf4/orjson-3.10.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dfbb2d460a855c9744bbc8e36f9c3a997c4b27d842f3d5559ed54326e6911f9b", size = 143186 }, - { url = "https://files.pythonhosted.org/packages/2c/f5/d835fee01a0284d4b78effc24d16e7609daac2ff6b6851ca1bdd3b6194fc/orjson-3.10.11-cp312-none-win32.whl", hash = "sha256:d4a62c49c506d4d73f59514986cadebb7e8d186ad510c518f439176cf8d5359d", size = 144489 }, - { url = "https://files.pythonhosted.org/packages/03/60/748e0e205060dec74328dfd835e47902eb5522ae011766da76bfff64e2f4/orjson-3.10.11-cp312-none-win_amd64.whl", hash = "sha256:f1eec3421a558ff7a9b010a6c7effcfa0ade65327a71bb9b02a1c3b77a247284", size = 136614 }, - { url = "https://files.pythonhosted.org/packages/13/92/400970baf46b987c058469e9e779fb7a40d54a5754914d3634cca417e054/orjson-3.10.11-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c46294faa4e4d0eb73ab68f1a794d2cbf7bab33b1dda2ac2959ffb7c61591899", size = 266402 }, - { url = "https://files.pythonhosted.org/packages/3c/fa/f126fc2d817552bd1f67466205abdcbff64eab16f6844fe6df2853528675/orjson-3.10.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52e5834d7d6e58a36846e059d00559cb9ed20410664f3ad156cd2cc239a11230", size = 140826 }, - { url = "https://files.pythonhosted.org/packages/ad/18/9b9664d7d4af5b4fe9fe6600b7654afc0684bba528260afdde10c4a530aa/orjson-3.10.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2fc947e5350fdce548bfc94f434e8760d5cafa97fb9c495d2fef6757aa02ec0", size = 142593 }, - { url = "https://files.pythonhosted.org/packages/20/f9/a30c68f12778d5e58e6b5cdd26f86ee2d0babce1a475073043f46fdd8402/orjson-3.10.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0efabbf839388a1dab5b72b5d3baedbd6039ac83f3b55736eb9934ea5494d258", size = 146777 }, - { url = "https://files.pythonhosted.org/packages/f2/97/12047b0c0e9b391d589fb76eb40538f522edc664f650f8e352fdaaf77ff5/orjson-3.10.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3f29634260708c200c4fe148e42b4aae97d7b9fee417fbdd74f8cfc265f15b0", size = 142961 }, - { url = "https://files.pythonhosted.org/packages/a4/97/d904e26c1cabf2dd6ab1b0909e9b790af28a7f0fcb9d8378d7320d4869eb/orjson-3.10.11-cp313-none-win32.whl", hash = "sha256:1a1222ffcee8a09476bbdd5d4f6f33d06d0d6642df2a3d78b7a195ca880d669b", size = 144486 }, - { url = "https://files.pythonhosted.org/packages/42/62/3760bd1e6e949321d99bab238d08db2b1266564d2f708af668f57109bb36/orjson-3.10.11-cp313-none-win_amd64.whl", hash = "sha256:bc274ac261cc69260913b2d1610760e55d3c0801bb3457ba7b9004420b6b4270", size = 136361 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, -] - -[[package]] -name = "pre-commit" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, -] - -[[package]] -name = "pycparser" -version = "2.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, -] - -[[package]] -name = "pydantic" -version = "2.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, -] - -[[package]] -name = "pydantic-core" -version = "2.23.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, - { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, - { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, - { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, - { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, - { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, - { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, - { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, - { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, - { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, - { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, - { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, - { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, - { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, - { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, - { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, - { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, - { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, - { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, - { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, - { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, - { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, - { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, - { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, - { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, - { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, - { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, - { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, - { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, - { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, - { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, - { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, - { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, - { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, - { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, - { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, -] - -[[package]] -name = "pygments" -version = "2.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/62/8336eff65bcbc8e4cb5d05b55faf041285951b6e80f33e2bff2024788f31/pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", size = 4891905 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/0b/32f05854cfd432e9286bb41a870e0d1a926b72df5f5cdb6dec962b2e369e/pymdown_extensions-10.12.tar.gz", hash = "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7", size = 840790 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/32/95a164ddf533bd676cbbe878e36e89b4ade3efde8dd61d0148c90cbbe57e/pymdown_extensions-10.12-py3-none-any.whl", hash = "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", size = 263448 }, -] - -[[package]] -name = "pytest" -version = "8.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 }, -] - -[[package]] -name = "pytest-cov" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, -] - -[[package]] -name = "pytest-mock" -version = "3.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, -] - -[[package]] -name = "pytest-randomly" -version = "3.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/68/d221ed7f4a2a49a664da721b8e87b52af6dd317af2a6cb51549cf17ac4b8/pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26", size = 13367 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/70/b31577d7c46d8e2f9baccfed5067dd8475262a2331ffb0bfdf19361c9bde/pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6", size = 8396 }, -] - -[[package]] -name = "pytest-timeout" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/93/0d/04719abc7a4bdb3a7a1f968f24b0f5253d698c9cc94975330e9d3145befb/pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9", size = 17697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - -[[package]] -name = "pywin32" -version = "308" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/e2/02652007469263fe1466e98439831d65d4ca80ea1a2df29abecedf7e47b7/pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a", size = 5928156 }, - { url = "https://files.pythonhosted.org/packages/48/ef/f4fb45e2196bc7ffe09cad0542d9aff66b0e33f6c0954b43e49c33cad7bd/pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b", size = 6559559 }, - { url = "https://files.pythonhosted.org/packages/79/ef/68bb6aa865c5c9b11a35771329e95917b5559845bd75b65549407f9fc6b4/pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6", size = 7972495 }, - { url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 }, - { url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 }, - { url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 }, - { url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 }, - { url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 }, - { url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 }, -] - -[[package]] -name = "pywin32-ctypes" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, -] - -[[package]] -name = "rcslice" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/3e/abe47d91d5340b77b003baf96fdf8966c946eb4c5a704a844b5d03e6e578/rcslice-1.1.0.tar.gz", hash = "sha256:a2ce70a60690eb63e52b722e046b334c3aaec5e900b28578f529878782ee5c6e", size = 4414 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/96/7935186fba032312eb8a75e6503440b0e6de76c901421f791408e4debd93/rcslice-1.1.0-py3-none-any.whl", hash = "sha256:1b12fc0c0ca452e8a9fd2b56ac008162f19e250906a4290a7e7a98be3200c2a6", size = 5180 }, -] - -[[package]] -name = "redis" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/17/2f4a87ffa4cd93714cf52edfa3ea94589e9de65f71e9f99cbcfa84347a53/redis-5.2.0.tar.gz", hash = "sha256:0b1087665a771b1ff2e003aa5bdd354f15a70c9e25d5a7dbf9c722c16528a7b0", size = 4607878 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/f5/ffa560ecc4bafbf25f7961c3d6f50d627a90186352e27e7d0ba5b1f6d87d/redis-5.2.0-py3-none-any.whl", hash = "sha256:ae174f2bb3b1bf2b09d54bf3e51fbc1469cf6c10aa03e21141f51969801a7897", size = 261428 }, -] - -[[package]] -name = "regex" -version = "2024.11.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/5f/bd69653fbfb76cf8604468d3b4ec4c403197144c7bfe0e6a5fc9e02a07cb/regex-2024.11.6.tar.gz", hash = "sha256:7ab159b063c52a0333c884e4679f8d7a85112ee3078fe3d9004b2dd875585519", size = 399494 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/58/7e4d9493a66c88a7da6d205768119f51af0f684fe7be7bac8328e217a52c/regex-2024.11.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5478c6962ad548b54a591778e93cd7c456a7a29f8eca9c49e4f9a806dcc5d638", size = 482669 }, - { url = "https://files.pythonhosted.org/packages/34/4c/8f8e631fcdc2ff978609eaeef1d6994bf2f028b59d9ac67640ed051f1218/regex-2024.11.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c89a8cc122b25ce6945f0423dc1352cb9593c68abd19223eebbd4e56612c5b7", size = 287684 }, - { url = "https://files.pythonhosted.org/packages/c5/1b/f0e4d13e6adf866ce9b069e191f303a30ab1277e037037a365c3aad5cc9c/regex-2024.11.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94d87b689cdd831934fa3ce16cc15cd65748e6d689f5d2b8f4f4df2065c9fa20", size = 284589 }, - { url = "https://files.pythonhosted.org/packages/25/4d/ab21047f446693887f25510887e6820b93f791992994f6498b0318904d4a/regex-2024.11.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1062b39a0a2b75a9c694f7a08e7183a80c63c0d62b301418ffd9c35f55aaa114", size = 792121 }, - { url = "https://files.pythonhosted.org/packages/45/ee/c867e15cd894985cb32b731d89576c41a4642a57850c162490ea34b78c3b/regex-2024.11.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:167ed4852351d8a750da48712c3930b031f6efdaa0f22fa1933716bfcd6bf4a3", size = 831275 }, - { url = "https://files.pythonhosted.org/packages/b3/12/b0f480726cf1c60f6536fa5e1c95275a77624f3ac8fdccf79e6727499e28/regex-2024.11.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d548dafee61f06ebdb584080621f3e0c23fff312f0de1afc776e2a2ba99a74f", size = 818257 }, - { url = "https://files.pythonhosted.org/packages/bf/ce/0d0e61429f603bac433910d99ef1a02ce45a8967ffbe3cbee48599e62d88/regex-2024.11.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a19f302cd1ce5dd01a9099aaa19cae6173306d1302a43b627f62e21cf18ac0", size = 792727 }, - { url = "https://files.pythonhosted.org/packages/e4/c1/243c83c53d4a419c1556f43777ccb552bccdf79d08fda3980e4e77dd9137/regex-2024.11.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bec9931dfb61ddd8ef2ebc05646293812cb6b16b60cf7c9511a832b6f1854b55", size = 780667 }, - { url = "https://files.pythonhosted.org/packages/c5/f4/75eb0dd4ce4b37f04928987f1d22547ddaf6c4bae697623c1b05da67a8aa/regex-2024.11.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9714398225f299aa85267fd222f7142fcb5c769e73d7733344efc46f2ef5cf89", size = 776963 }, - { url = "https://files.pythonhosted.org/packages/16/5d/95c568574e630e141a69ff8a254c2f188b4398e813c40d49228c9bbd9875/regex-2024.11.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:202eb32e89f60fc147a41e55cb086db2a3f8cb82f9a9a88440dcfc5d37faae8d", size = 784700 }, - { url = "https://files.pythonhosted.org/packages/8e/b5/f8495c7917f15cc6fee1e7f395e324ec3e00ab3c665a7dc9d27562fd5290/regex-2024.11.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4181b814e56078e9b00427ca358ec44333765f5ca1b45597ec7446d3a1ef6e34", size = 848592 }, - { url = "https://files.pythonhosted.org/packages/1c/80/6dd7118e8cb212c3c60b191b932dc57db93fb2e36fb9e0e92f72a5909af9/regex-2024.11.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:068376da5a7e4da51968ce4c122a7cd31afaaec4fccc7856c92f63876e57b51d", size = 852929 }, - { url = "https://files.pythonhosted.org/packages/11/9b/5a05d2040297d2d254baf95eeeb6df83554e5e1df03bc1a6687fc4ba1f66/regex-2024.11.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f2c4184420d881a3475fb2c6f4d95d53a8d50209a2500723d831036f7c45", size = 781213 }, - { url = "https://files.pythonhosted.org/packages/26/b7/b14e2440156ab39e0177506c08c18accaf2b8932e39fb092074de733d868/regex-2024.11.6-cp311-cp311-win32.whl", hash = "sha256:c36f9b6f5f8649bb251a5f3f66564438977b7ef8386a52460ae77e6070d309d9", size = 261734 }, - { url = "https://files.pythonhosted.org/packages/80/32/763a6cc01d21fb3819227a1cc3f60fd251c13c37c27a73b8ff4315433a8e/regex-2024.11.6-cp311-cp311-win_amd64.whl", hash = "sha256:02e28184be537f0e75c1f9b2f8847dc51e08e6e171c6bde130b2687e0c33cf60", size = 274052 }, - { url = "https://files.pythonhosted.org/packages/ba/30/9a87ce8336b172cc232a0db89a3af97929d06c11ceaa19d97d84fa90a8f8/regex-2024.11.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:52fb28f528778f184f870b7cf8f225f5eef0a8f6e3778529bdd40c7b3920796a", size = 483781 }, - { url = "https://files.pythonhosted.org/packages/01/e8/00008ad4ff4be8b1844786ba6636035f7ef926db5686e4c0f98093612add/regex-2024.11.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdd6028445d2460f33136c55eeb1f601ab06d74cb3347132e1c24250187500d9", size = 288455 }, - { url = "https://files.pythonhosted.org/packages/60/85/cebcc0aff603ea0a201667b203f13ba75d9fc8668fab917ac5b2de3967bc/regex-2024.11.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805e6b60c54bf766b251e94526ebad60b7de0c70f70a4e6210ee2891acb70bf2", size = 284759 }, - { url = "https://files.pythonhosted.org/packages/94/2b/701a4b0585cb05472a4da28ee28fdfe155f3638f5e1ec92306d924e5faf0/regex-2024.11.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b85c2530be953a890eaffde05485238f07029600e8f098cdf1848d414a8b45e4", size = 794976 }, - { url = "https://files.pythonhosted.org/packages/4b/bf/fa87e563bf5fee75db8915f7352e1887b1249126a1be4813837f5dbec965/regex-2024.11.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb26437975da7dc36b7efad18aa9dd4ea569d2357ae6b783bf1118dabd9ea577", size = 833077 }, - { url = "https://files.pythonhosted.org/packages/a1/56/7295e6bad94b047f4d0834e4779491b81216583c00c288252ef625c01d23/regex-2024.11.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abfa5080c374a76a251ba60683242bc17eeb2c9818d0d30117b4486be10c59d3", size = 823160 }, - { url = "https://files.pythonhosted.org/packages/fb/13/e3b075031a738c9598c51cfbc4c7879e26729c53aa9cca59211c44235314/regex-2024.11.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b7fa6606c2881c1db9479b0eaa11ed5dfa11c8d60a474ff0e095099f39d98e", size = 796896 }, - { url = "https://files.pythonhosted.org/packages/24/56/0b3f1b66d592be6efec23a795b37732682520b47c53da5a32c33ed7d84e3/regex-2024.11.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c32f75920cf99fe6b6c539c399a4a128452eaf1af27f39bce8909c9a3fd8cbe", size = 783997 }, - { url = "https://files.pythonhosted.org/packages/f9/a1/eb378dada8b91c0e4c5f08ffb56f25fcae47bf52ad18f9b2f33b83e6d498/regex-2024.11.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:982e6d21414e78e1f51cf595d7f321dcd14de1f2881c5dc6a6e23bbbbd68435e", size = 781725 }, - { url = "https://files.pythonhosted.org/packages/83/f2/033e7dec0cfd6dda93390089864732a3409246ffe8b042e9554afa9bff4e/regex-2024.11.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a7c2155f790e2fb448faed6dd241386719802296ec588a8b9051c1f5c481bc29", size = 789481 }, - { url = "https://files.pythonhosted.org/packages/83/23/15d4552ea28990a74e7696780c438aadd73a20318c47e527b47a4a5a596d/regex-2024.11.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:149f5008d286636e48cd0b1dd65018548944e495b0265b45e1bffecce1ef7f39", size = 852896 }, - { url = "https://files.pythonhosted.org/packages/e3/39/ed4416bc90deedbfdada2568b2cb0bc1fdb98efe11f5378d9892b2a88f8f/regex-2024.11.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e5364a4502efca094731680e80009632ad6624084aff9a23ce8c8c6820de3e51", size = 860138 }, - { url = "https://files.pythonhosted.org/packages/93/2d/dd56bb76bd8e95bbce684326302f287455b56242a4f9c61f1bc76e28360e/regex-2024.11.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0a86e7eeca091c09e021db8eb72d54751e527fa47b8d5787caf96d9831bd02ad", size = 787692 }, - { url = "https://files.pythonhosted.org/packages/0b/55/31877a249ab7a5156758246b9c59539abbeba22461b7d8adc9e8475ff73e/regex-2024.11.6-cp312-cp312-win32.whl", hash = "sha256:32f9a4c643baad4efa81d549c2aadefaeba12249b2adc5af541759237eee1c54", size = 262135 }, - { url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567 }, - { url = "https://files.pythonhosted.org/packages/90/73/bcb0e36614601016552fa9344544a3a2ae1809dc1401b100eab02e772e1f/regex-2024.11.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a6ba92c0bcdf96cbf43a12c717eae4bc98325ca3730f6b130ffa2e3c3c723d84", size = 483525 }, - { url = "https://files.pythonhosted.org/packages/0f/3f/f1a082a46b31e25291d830b369b6b0c5576a6f7fb89d3053a354c24b8a83/regex-2024.11.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:525eab0b789891ac3be914d36893bdf972d483fe66551f79d3e27146191a37d4", size = 288324 }, - { url = "https://files.pythonhosted.org/packages/09/c9/4e68181a4a652fb3ef5099e077faf4fd2a694ea6e0f806a7737aff9e758a/regex-2024.11.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:086a27a0b4ca227941700e0b31425e7a28ef1ae8e5e05a33826e17e47fbfdba0", size = 284617 }, - { url = "https://files.pythonhosted.org/packages/fc/fd/37868b75eaf63843165f1d2122ca6cb94bfc0271e4428cf58c0616786dce/regex-2024.11.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde01f35767c4a7899b7eb6e823b125a64de314a8ee9791367c9a34d56af18d0", size = 795023 }, - { url = "https://files.pythonhosted.org/packages/c4/7c/d4cd9c528502a3dedb5c13c146e7a7a539a3853dc20209c8e75d9ba9d1b2/regex-2024.11.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b583904576650166b3d920d2bcce13971f6f9e9a396c673187f49811b2769dc7", size = 833072 }, - { url = "https://files.pythonhosted.org/packages/4f/db/46f563a08f969159c5a0f0e722260568425363bea43bb7ae370becb66a67/regex-2024.11.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c4de13f06a0d54fa0d5ab1b7138bfa0d883220965a29616e3ea61b35d5f5fc7", size = 823130 }, - { url = "https://files.pythonhosted.org/packages/db/60/1eeca2074f5b87df394fccaa432ae3fc06c9c9bfa97c5051aed70e6e00c2/regex-2024.11.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cde6e9f2580eb1665965ce9bf17ff4952f34f5b126beb509fee8f4e994f143c", size = 796857 }, - { url = "https://files.pythonhosted.org/packages/10/db/ac718a08fcee981554d2f7bb8402f1faa7e868c1345c16ab1ebec54b0d7b/regex-2024.11.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d7f453dca13f40a02b79636a339c5b62b670141e63efd511d3f8f73fba162b3", size = 784006 }, - { url = "https://files.pythonhosted.org/packages/c2/41/7da3fe70216cea93144bf12da2b87367590bcf07db97604edeea55dac9ad/regex-2024.11.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59dfe1ed21aea057a65c6b586afd2a945de04fc7db3de0a6e3ed5397ad491b07", size = 781650 }, - { url = "https://files.pythonhosted.org/packages/a7/d5/880921ee4eec393a4752e6ab9f0fe28009435417c3102fc413f3fe81c4e5/regex-2024.11.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b97c1e0bd37c5cd7902e65f410779d39eeda155800b65fc4d04cc432efa9bc6e", size = 789545 }, - { url = "https://files.pythonhosted.org/packages/dc/96/53770115e507081122beca8899ab7f5ae28ae790bfcc82b5e38976df6a77/regex-2024.11.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d1e379028e0fc2ae3654bac3cbbef81bf3fd571272a42d56c24007979bafb6", size = 853045 }, - { url = "https://files.pythonhosted.org/packages/31/d3/1372add5251cc2d44b451bd94f43b2ec78e15a6e82bff6a290ef9fd8f00a/regex-2024.11.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:13291b39131e2d002a7940fb176e120bec5145f3aeb7621be6534e46251912c4", size = 860182 }, - { url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 }, - { url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 }, - { url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "rich" -version = "13.9.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", size = 222889 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", size = 242157 }, -] - -[[package]] -name = "ruff" -version = "0.7.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, - { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, - { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, - { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, - { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, - { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, - { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, - { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, - { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, - { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, - { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, - { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, - { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, - { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, - { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, - { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, - { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, -] - -[[package]] -name = "secretstorage" -version = "3.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221 }, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, -] - -[[package]] -name = "six" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "starlette" -version = "0.41.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, -] - -[[package]] -name = "testcontainers" -version = "4.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docker" }, - { name = "typing-extensions" }, - { name = "urllib3" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/72/c58d84f5704c6caadd9f803a3adad5ab54ac65328c02d13295f40860cf33/testcontainers-4.8.2.tar.gz", hash = "sha256:dd4a6a2ea09e3c3ecd39e180b6548105929d0bb78d665ce9919cb3f8c98f9853", size = 63590 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/77/5ac0dff2903a033d83d971fd85957356abdb66a327f3589df2b3d1a586b4/testcontainers-4.8.2-py3-none-any.whl", hash = "sha256:9e19af077cd96e1957c13ee466f1f32905bc6c5bc1bc98643eb18be1a989bfb0", size = 104326 }, -] - -[package.optional-dependencies] -redis = [ - { name = "redis" }, -] - -[[package]] -name = "tomli" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/b9/de2a5c0144d7d75a57ff355c0c24054f965b2dc3036456ae03a51ea6264b/tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed", size = 16096 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/db/ce8eda256fa131af12e0a76d481711abe4681b6923c27efb9a255c9e4594/tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38", size = 13237 }, -] - -[[package]] -name = "tomli-w" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/19/b65f1a088ee23e37cdea415b357843eca8b1422a7b11a9eee6e35d4ec273/tomli_w-1.1.0.tar.gz", hash = "sha256:49e847a3a304d516a169a601184932ef0f6b61623fe680f836a2aa7128ed0d33", size = 6929 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ac/ce90573ba446a9bbe65838ded066a805234d159b4446ae9f8ec5bbd36cbd/tomli_w-1.1.0-py3-none-any.whl", hash = "sha256:1403179c78193e3184bfaade390ddbd071cba48a32a2e62ba11aae47490c63f7", size = 6440 }, -] - -[[package]] -name = "tomlkit" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, -] - -[[package]] -name = "trove-classifiers" -version = "2024.10.21.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/85/92c2667cf221b37648041ce9319427f92fa76cbec634aad844e67e284706/trove_classifiers-2024.10.21.16.tar.gz", hash = "sha256:17cbd055d67d5e9d9de63293a8732943fabc21574e4c7b74edf112b4928cf5f3", size = 16153 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/35/5055ab8d215af853d07bbff1a74edf48f91ed308f037380a5ca52dd73348/trove_classifiers-2024.10.21.16-py3-none-any.whl", hash = "sha256:0fb11f1e995a757807a8ef1c03829fbd4998d817319abcef1f33165750f103be", size = 13546 }, -] - -[[package]] -name = "typer" -version = "0.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/58/a79003b91ac2c6890fc5d90145c662fd5771c6f11447f116b63300436bc9/typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722", size = 98953 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/2b/886d13e742e514f704c33c4caa7df0f3b89e5a25ef8db02aa9ca3d9535d5/typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b", size = 47288 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "urllib3" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, -] - -[[package]] -name = "userpath" -version = "1.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/b7/30753098208505d7ff9be5b3a32112fb8a4cb3ddfccbbb7ba9973f2e29ff/userpath-1.9.2.tar.gz", hash = "sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815", size = 11140 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/99/3ec6335ded5b88c2f7ed25c56ffd952546f7ed007ffb1e1539dc3b57015a/userpath-1.9.2-py3-none-any.whl", hash = "sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d", size = 9065 }, -] - -[[package]] -name = "uv" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/ad/66cc8e00c217e7fcf76598c880632b480aa38d4cad311596b78e99737498/uv-0.5.4.tar.gz", hash = "sha256:cd7a5a3a36f975a7678f27849a2d49bafe7272143d938e9b6f3bf28392a3ba00", size = 2315678 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/3e/6bf24d7bb0d11715ea783ecabcacdecdc8c51fca0144fcdad2090d65bae5/uv-0.5.4-py3-none-linux_armv6l.whl", hash = "sha256:2118bb99cbc9787cb5e5cc4a507201e25a3fe88a9f389e8ffb84f242d96038c2", size = 13853445 }, - { url = "https://files.pythonhosted.org/packages/b8/be/c3acbe2944cd694a5d61a7a461468fa886512c84014545bb8f3244092eaa/uv-0.5.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4432215deb8d5c1ccab17ee51cb80f5de1a20865ee02df47532f87442a3d6a58", size = 13969300 }, - { url = "https://files.pythonhosted.org/packages/1f/c5/06e3b93045179b92d75cf94e6e224baec3226070f1cbc0e11d4898300b54/uv-0.5.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f40c6c6c3a1b398b56d3a8b28f7b455ac1ce4cbb1469f8d35d3bbc804d83daa4", size = 12932325 }, - { url = "https://files.pythonhosted.org/packages/b8/f9/06ab86e9f0c270c495077ef2b588458172ed84f9c337de725c8b08872354/uv-0.5.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:df3cb58b7da91f4fc647d09c3e96006cd6c7bd424a81ce2308a58593c6887c39", size = 13183356 }, - { url = "https://files.pythonhosted.org/packages/c1/cb/bee01ef23e5020dc1f12d86ca8f82e95a723585db3ec64bfab4016e5616c/uv-0.5.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd2df2ba823e6684230ab4c581f2320be38d7f46de11ce21d2dbba631470d7b6", size = 13622310 }, - { url = "https://files.pythonhosted.org/packages/19/4b/128fd874151919c71af51f528db28964e6d8e509fff12210ec9ba99b13fb/uv-0.5.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:928ed95fefe4e1338d0a7ad2f6b635de59e2ec92adaed4a267f7501a3b252263", size = 14207832 }, - { url = "https://files.pythonhosted.org/packages/b1/2b/0fed8a49440494f6806dcb67021ca8f14d46f45a665235fc153791e19574/uv-0.5.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:05b45c7eefb178dcdab0d49cd642fb7487377d00727102a8d6d306cc034c0d83", size = 14878796 }, - { url = "https://files.pythonhosted.org/packages/c9/35/a6dc404d4d8884e26ad7bda004c101972fe7d81f86546a8628272812b897/uv-0.5.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed5659cde099f39995f4cb793fd939d2260b4a26e4e29412c91e7537f53d8d25", size = 14687838 }, - { url = "https://files.pythonhosted.org/packages/74/9e/c2ebf66b90d48def06cda29626bb38068418ed135ca903beb293825ef66d/uv-0.5.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f07e5e0df40a09154007da41b76932671333f9fecb0735c698b19da25aa08927", size = 18960541 }, - { url = "https://files.pythonhosted.org/packages/3d/67/28a8b4c23920ae1b1b0103ebae2fa176bd5677c4353b5e814a51bd183285/uv-0.5.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30ce031e36c54d4ba791d743d992d0a4fd8d70480db781d30a2f6f5125f39194", size = 14471756 }, - { url = "https://files.pythonhosted.org/packages/e9/1c/9698818f4c5493dfd5ab0899a90eee789cac214de2f171220bcdfaefc93a/uv-0.5.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ca72e6a4c3c6b8b5605867e16a7f767f5c99b7f526de6bbb903c60eb44fd1e01", size = 13389089 }, - { url = "https://files.pythonhosted.org/packages/0b/30/31a9985d84ffb63fb9212fa2b565497e0ceb581be055e5cc760afbe26b11/uv-0.5.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:69079e900bd26b0f65069ac6fa684c74662ed87121c076f2b1cbcf042539034c", size = 13612748 }, - { url = "https://files.pythonhosted.org/packages/26/8d/bae613187ba88d74f0268246ce140f23d399bab96d2cbc055d6e4adafd09/uv-0.5.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:8d7a4a3df943a7c16cd032ccbaab8ed21ff64f4cb090b3a0a15a8b7502ccd876", size = 13946421 }, - { url = "https://files.pythonhosted.org/packages/0e/22/efd1eec81a566139bced68f4bd140c275edac3dac1bd6236cf8d756423db/uv-0.5.4-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:f511faf719b797ef0f14688f1abe20b3fd126209cf58512354d1813249745119", size = 15752913 }, - { url = "https://files.pythonhosted.org/packages/49/b2/0cc4ae143b9605c25e75772aea22876b5875db79982ba62bb6f8d3099fab/uv-0.5.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f806af0ee451a81099c449c4cff0e813056fdf7dd264f3d3a8fd321b17ff9efc", size = 14599503 }, - { url = "https://files.pythonhosted.org/packages/51/9a/33d40a5068fd37c4f7b4fa82396e3ee90a691cd256f364ff398612c1d5d4/uv-0.5.4-py3-none-win32.whl", hash = "sha256:a79a0885df364b897da44aae308e6ed9cca3a189d455cf1c205bd6f7b03daafa", size = 13749570 }, - { url = "https://files.pythonhosted.org/packages/b1/c8/827e4da65cbdab2c1619767a68ab99a31de078e511b71ca9f24777df33f9/uv-0.5.4-py3-none-win_amd64.whl", hash = "sha256:493aedc3c758bbaede83ecc8d5f7e6a9279ebec151c7f756aa9ea898c73f8ddb", size = 15573613 }, -] - -[[package]] -name = "uvicorn" -version = "0.32.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/fc/1d785078eefd6945f3e5bab5c076e4230698046231eb0f3747bc5c8fa992/uvicorn-0.32.0.tar.gz", hash = "sha256:f78b36b143c16f54ccdb8190d0a26b5f1901fe5a3c777e1ab29f26391af8551e", size = 77564 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/14/78bd0e95dd2444b6caacbca2b730671d4295ccb628ef58b81bee903629df/uvicorn-0.32.0-py3-none-any.whl", hash = "sha256:60b8f3a5ac027dcd31448f411ced12b5ef452c646f76f02f8cc3f25d8d26fd82", size = 63723 }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410 }, - { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476 }, - { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855 }, - { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185 }, - { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256 }, - { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323 }, - { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 }, - { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 }, - { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 }, - { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 }, - { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 }, - { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 }, - { url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 }, - { url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 }, - { url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 }, - { url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 }, - { url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 }, - { url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 }, -] - -[[package]] -name = "virtualenv" -version = "20.27.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/b3/7b6a79c5c8cf6d90ea681310e169cf2db2884f4d583d16c6e1d5a75a4e04/virtualenv-20.27.1.tar.gz", hash = "sha256:142c6be10212543b32c6c45d3d3893dff89112cc588b7d0879ae5a1ec03a47ba", size = 6491145 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/92/78324ff89391e00c8f4cf6b8526c41c6ef36b4ea2d2c132250b1a6fc2b8d/virtualenv-20.27.1-py3-none-any.whl", hash = "sha256:f11f1b8a29525562925f745563bfd48b189450f61fb34c4f9cc79dd5aa32a1f4", size = 3117838 }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393 }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392 }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019 }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471 }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449 }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054 }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, -] - -[[package]] -name = "watchfiles" -version = "0.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c8/27/2ba23c8cc85796e2d41976439b08d52f691655fdb9401362099502d1f0cf/watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1", size = 37870 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/02/366ae902cd81ca5befcd1854b5c7477b378f68861597cef854bd6dc69fbe/watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428", size = 375579 }, - { url = "https://files.pythonhosted.org/packages/bc/67/d8c9d256791fe312fea118a8a051411337c948101a24586e2df237507976/watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c", size = 367726 }, - { url = "https://files.pythonhosted.org/packages/b1/dc/a8427b21ef46386adf824a9fec4be9d16a475b850616cfd98cf09a97a2ef/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43", size = 437735 }, - { url = "https://files.pythonhosted.org/packages/3a/21/0b20bef581a9fbfef290a822c8be645432ceb05fb0741bf3c032e0d90d9a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327", size = 433644 }, - { url = "https://files.pythonhosted.org/packages/1c/e8/d5e5f71cc443c85a72e70b24269a30e529227986096abe091040d6358ea9/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5", size = 450928 }, - { url = "https://files.pythonhosted.org/packages/61/ee/bf17f5a370c2fcff49e1fec987a6a43fd798d8427ea754ce45b38f9e117a/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61", size = 469072 }, - { url = "https://files.pythonhosted.org/packages/a3/34/03b66d425986de3fc6077e74a74c78da298f8cb598887f664a4485e55543/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15", size = 475517 }, - { url = "https://files.pythonhosted.org/packages/70/eb/82f089c4f44b3171ad87a1b433abb4696f18eb67292909630d886e073abe/watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823", size = 425480 }, - { url = "https://files.pythonhosted.org/packages/53/20/20509c8f5291e14e8a13104b1808cd7cf5c44acd5feaecb427a49d387774/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab", size = 612322 }, - { url = "https://files.pythonhosted.org/packages/df/2b/5f65014a8cecc0a120f5587722068a975a692cadbe9fe4ea56b3d8e43f14/watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec", size = 595094 }, - { url = "https://files.pythonhosted.org/packages/18/98/006d8043a82c0a09d282d669c88e587b3a05cabdd7f4900e402250a249ac/watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d", size = 264191 }, - { url = "https://files.pythonhosted.org/packages/8a/8b/badd9247d6ec25f5f634a9b3d0d92e39c045824ec7e8afcedca8ee52c1e2/watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c", size = 277527 }, - { url = "https://files.pythonhosted.org/packages/af/19/35c957c84ee69d904299a38bae3614f7cede45f07f174f6d5a2f4dbd6033/watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633", size = 266253 }, - { url = "https://files.pythonhosted.org/packages/35/82/92a7bb6dc82d183e304a5f84ae5437b59ee72d48cee805a9adda2488b237/watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a", size = 374137 }, - { url = "https://files.pythonhosted.org/packages/87/91/49e9a497ddaf4da5e3802d51ed67ff33024597c28f652b8ab1e7c0f5718b/watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370", size = 367733 }, - { url = "https://files.pythonhosted.org/packages/0d/d8/90eb950ab4998effea2df4cf3a705dc594f6bc501c5a353073aa990be965/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6", size = 437322 }, - { url = "https://files.pythonhosted.org/packages/6c/a2/300b22e7bc2a222dd91fce121cefa7b49aa0d26a627b2777e7bdfcf1110b/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b", size = 433409 }, - { url = "https://files.pythonhosted.org/packages/99/44/27d7708a43538ed6c26708bcccdde757da8b7efb93f4871d4cc39cffa1cc/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e", size = 452142 }, - { url = "https://files.pythonhosted.org/packages/b0/ec/c4e04f755be003129a2c5f3520d2c47026f00da5ecb9ef1e4f9449637571/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea", size = 469414 }, - { url = "https://files.pythonhosted.org/packages/c5/4e/cdd7de3e7ac6432b0abf282ec4c1a1a2ec62dfe423cf269b86861667752d/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f", size = 472962 }, - { url = "https://files.pythonhosted.org/packages/27/69/e1da9d34da7fc59db358424f5d89a56aaafe09f6961b64e36457a80a7194/watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234", size = 425705 }, - { url = "https://files.pythonhosted.org/packages/e8/c1/24d0f7357be89be4a43e0a656259676ea3d7a074901f47022f32e2957798/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef", size = 612851 }, - { url = "https://files.pythonhosted.org/packages/c7/af/175ba9b268dec56f821639c9893b506c69fd999fe6a2e2c51de420eb2f01/watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968", size = 594868 }, - { url = "https://files.pythonhosted.org/packages/44/81/1f701323a9f70805bc81c74c990137123344a80ea23ab9504a99492907f8/watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444", size = 264109 }, - { url = "https://files.pythonhosted.org/packages/b4/0b/32cde5bc2ebd9f351be326837c61bdeb05ad652b793f25c91cac0b48a60b/watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896", size = 277055 }, - { url = "https://files.pythonhosted.org/packages/4b/81/daade76ce33d21dbec7a15afd7479de8db786e5f7b7d249263b4ea174e08/watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418", size = 266169 }, - { url = "https://files.pythonhosted.org/packages/30/dc/6e9f5447ae14f645532468a84323a942996d74d5e817837a5c8ce9d16c69/watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48", size = 373764 }, - { url = "https://files.pythonhosted.org/packages/79/c0/c3a9929c372816c7fc87d8149bd722608ea58dc0986d3ef7564c79ad7112/watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90", size = 367873 }, - { url = "https://files.pythonhosted.org/packages/2e/11/ff9a4445a7cfc1c98caf99042df38964af12eed47d496dd5d0d90417349f/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94", size = 438381 }, - { url = "https://files.pythonhosted.org/packages/48/a3/763ba18c98211d7bb6c0f417b2d7946d346cdc359d585cc28a17b48e964b/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e", size = 432809 }, - { url = "https://files.pythonhosted.org/packages/30/4c/616c111b9d40eea2547489abaf4ffc84511e86888a166d3a4522c2ba44b5/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827", size = 451801 }, - { url = "https://files.pythonhosted.org/packages/b6/be/d7da83307863a422abbfeb12903a76e43200c90ebe5d6afd6a59d158edea/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df", size = 468886 }, - { url = "https://files.pythonhosted.org/packages/1d/d3/3dfe131ee59d5e90b932cf56aba5c996309d94dafe3d02d204364c23461c/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab", size = 472973 }, - { url = "https://files.pythonhosted.org/packages/42/6c/279288cc5653a289290d183b60a6d80e05f439d5bfdfaf2d113738d0f932/watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f", size = 425282 }, - { url = "https://files.pythonhosted.org/packages/d6/d7/58afe5e85217e845edf26d8780c2d2d2ae77675eeb8d1b8b8121d799ce52/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b", size = 612540 }, - { url = "https://files.pythonhosted.org/packages/6d/d5/b96eeb9fe3fda137200dd2f31553670cbc731b1e13164fd69b49870b76ec/watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18", size = 593625 }, - { url = "https://files.pythonhosted.org/packages/c1/e5/c326fe52ee0054107267608d8cea275e80be4455b6079491dfd9da29f46f/watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07", size = 263899 }, - { url = "https://files.pythonhosted.org/packages/a6/8b/8a7755c5e7221bb35fe4af2dc44db9174f90ebf0344fd5e9b1e8b42d381e/watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366", size = 276622 }, -] - -[[package]] -name = "websockets" -version = "14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/ed/c0d03cb607b7fe1f7ff45e2cd4bb5cd0f9e3299ced79c2c303a6fff44524/websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512", size = 161949 }, - { url = "https://files.pythonhosted.org/packages/06/91/bf0a44e238660d37a2dda1b4896235d20c29a2d0450f3a46cd688f43b239/websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac", size = 159606 }, - { url = "https://files.pythonhosted.org/packages/ff/b8/7185212adad274c2b42b6a24e1ee6b916b7809ed611cbebc33b227e5c215/websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280", size = 159854 }, - { url = "https://files.pythonhosted.org/packages/5a/8a/0849968d83474be89c183d8ae8dcb7f7ada1a3c24f4d2a0d7333c231a2c3/websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1", size = 169402 }, - { url = "https://files.pythonhosted.org/packages/bd/4f/ef886e37245ff6b4a736a09b8468dae05d5d5c99de1357f840d54c6f297d/websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3", size = 168406 }, - { url = "https://files.pythonhosted.org/packages/11/43/e2dbd4401a63e409cebddedc1b63b9834de42f51b3c84db885469e9bdcef/websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6", size = 168776 }, - { url = "https://files.pythonhosted.org/packages/6d/d6/7063e3f5c1b612e9f70faae20ebaeb2e684ffa36cb959eb0862ee2809b32/websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0", size = 169083 }, - { url = "https://files.pythonhosted.org/packages/49/69/e6f3d953f2fa0f8a723cf18cd011d52733bd7f6e045122b24e0e7f49f9b0/websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89", size = 168529 }, - { url = "https://files.pythonhosted.org/packages/70/ff/f31fa14561fc1d7b8663b0ed719996cf1f581abee32c8fb2f295a472f268/websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23", size = 168475 }, - { url = "https://files.pythonhosted.org/packages/f1/15/b72be0e4bf32ff373aa5baef46a4c7521b8ea93ad8b49ca8c6e8e764c083/websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e", size = 162833 }, - { url = "https://files.pythonhosted.org/packages/bc/ef/2d81679acbe7057ffe2308d422f744497b52009ea8bab34b6d74a2657d1d/websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09", size = 163263 }, - { url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 }, - { url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 }, - { url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 }, - { url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 }, - { url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 }, - { url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 }, - { url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 }, - { url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 }, - { url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 }, - { url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 }, - { url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 }, - { url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 }, - { url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 }, - { url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 }, - { url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 }, - { url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 }, - { url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 }, - { url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 }, - { url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 }, - { url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 }, - { url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 }, - { url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 }, - { url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 }, -] - -[[package]] -name = "win32-setctime" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/dd/f95a13d2b235a28d613ba23ebad55191514550debb968b46aab99f2e3a30/win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2", size = 3676 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/e6/a7d828fef907843b2a5773ebff47fb79ac0c1c88d60c0ca9530ee941e248/win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad", size = 3604 }, -] - -[[package]] -name = "wrapt" -version = "1.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/4c/063a912e20bcef7124e0df97282a8af3ff3e4b603ce84c481d6d7346be0a/wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", size = 53972 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/03/c188ac517f402775b90d6f312955a5e53b866c964b32119f2ed76315697e/wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", size = 37313 }, - { url = "https://files.pythonhosted.org/packages/0f/16/ea627d7817394db04518f62934a5de59874b587b792300991b3c347ff5e0/wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", size = 38164 }, - { url = "https://files.pythonhosted.org/packages/7f/a7/f1212ba098f3de0fd244e2de0f8791ad2539c03bef6c05a9fcb03e45b089/wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", size = 80890 }, - { url = "https://files.pythonhosted.org/packages/b7/96/bb5e08b3d6db003c9ab219c487714c13a237ee7dcc572a555eaf1ce7dc82/wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", size = 73118 }, - { url = "https://files.pythonhosted.org/packages/6e/52/2da48b35193e39ac53cfb141467d9f259851522d0e8c87153f0ba4205fb1/wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", size = 80746 }, - { url = "https://files.pythonhosted.org/packages/11/fb/18ec40265ab81c0e82a934de04596b6ce972c27ba2592c8b53d5585e6bcd/wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", size = 85668 }, - { url = "https://files.pythonhosted.org/packages/0f/ef/0ecb1fa23145560431b970418dce575cfaec555ab08617d82eb92afc7ccf/wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", size = 78556 }, - { url = "https://files.pythonhosted.org/packages/25/62/cd284b2b747f175b5a96cbd8092b32e7369edab0644c45784871528eb852/wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", size = 85712 }, - { url = "https://files.pythonhosted.org/packages/e5/a7/47b7ff74fbadf81b696872d5ba504966591a3468f1bc86bca2f407baef68/wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", size = 35327 }, - { url = "https://files.pythonhosted.org/packages/cf/c3/0084351951d9579ae83a3d9e38c140371e4c6b038136909235079f2e6e78/wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", size = 37523 }, - { url = "https://files.pythonhosted.org/packages/92/17/224132494c1e23521868cdd57cd1e903f3b6a7ba6996b7b8f077ff8ac7fe/wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", size = 37614 }, - { url = "https://files.pythonhosted.org/packages/6a/d7/cfcd73e8f4858079ac59d9db1ec5a1349bc486ae8e9ba55698cc1f4a1dff/wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", size = 38316 }, - { url = "https://files.pythonhosted.org/packages/7e/79/5ff0a5c54bda5aec75b36453d06be4f83d5cd4932cc84b7cb2b52cee23e2/wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", size = 86322 }, - { url = "https://files.pythonhosted.org/packages/c4/81/e799bf5d419f422d8712108837c1d9bf6ebe3cb2a81ad94413449543a923/wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", size = 79055 }, - { url = "https://files.pythonhosted.org/packages/62/62/30ca2405de6a20448ee557ab2cd61ab9c5900be7cbd18a2639db595f0b98/wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", size = 87291 }, - { url = "https://files.pythonhosted.org/packages/49/4e/5d2f6d7b57fc9956bf06e944eb00463551f7d52fc73ca35cfc4c2cdb7aed/wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", size = 90374 }, - { url = "https://files.pythonhosted.org/packages/a6/9b/c2c21b44ff5b9bf14a83252a8b973fb84923764ff63db3e6dfc3895cf2e0/wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", size = 83896 }, - { url = "https://files.pythonhosted.org/packages/14/26/93a9fa02c6f257df54d7570dfe8011995138118d11939a4ecd82cb849613/wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", size = 91738 }, - { url = "https://files.pythonhosted.org/packages/a2/5b/4660897233eb2c8c4de3dc7cefed114c61bacb3c28327e64150dc44ee2f6/wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", size = 35568 }, - { url = "https://files.pythonhosted.org/packages/5c/cc/8297f9658506b224aa4bd71906447dea6bb0ba629861a758c28f67428b91/wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", size = 37653 }, - { url = "https://files.pythonhosted.org/packages/ff/21/abdedb4cdf6ff41ebf01a74087740a709e2edb146490e4d9beea054b0b7a/wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", size = 23362 }, -] - -[[package]] -name = "zipp" -version = "3.21.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, -] - -[[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 }, -] diff --git a/.auto-resolution/.github/workflows/ci.yml b/.github/workflows/ci.yml similarity index 100% rename from .auto-resolution/.github/workflows/ci.yml rename to .github/workflows/ci.yml diff --git a/.auto-resolution/.github/workflows/release.yml b/.github/workflows/release.yml similarity index 100% rename from .auto-resolution/.github/workflows/release.yml rename to .github/workflows/release.yml diff --git a/.auto-resolution/.gitignore b/.gitignore similarity index 100% rename from .auto-resolution/.gitignore rename to .gitignore diff --git a/.auto-resolution/.pre-commit-config.yaml b/.pre-commit-config.yaml similarity index 100% rename from .auto-resolution/.pre-commit-config.yaml rename to .pre-commit-config.yaml diff --git a/.auto-resolution/.vscode/settings.json b/.vscode/settings.json similarity index 100% rename from .auto-resolution/.vscode/settings.json rename to .vscode/settings.json diff --git a/.auto-resolution/LICENSE b/LICENSE similarity index 100% rename from .auto-resolution/LICENSE rename to LICENSE diff --git a/.auto-resolution/README.md b/README.md similarity index 100% rename from .auto-resolution/README.md rename to README.md diff --git a/README.txt b/README.txt deleted file mode 100644 index 2af04b7..0000000 --- a/README.txt +++ /dev/null @@ -1 +0,0 @@ -You have checked out a GitButler Conflicted commit. You probably didn't mean to do this. \ No newline at end of file diff --git a/.auto-resolution/docs/index.md b/docs/index.md similarity index 100% rename from .auto-resolution/docs/index.md rename to docs/index.md diff --git a/.auto-resolution/docs/logging.md b/docs/logging.md similarity index 100% rename from .auto-resolution/docs/logging.md rename to docs/logging.md diff --git a/.auto-resolution/docs/sync.md b/docs/sync.md similarity index 100% rename from .auto-resolution/docs/sync.md rename to docs/sync.md diff --git a/.auto-resolution/docs/task.md b/docs/task.md similarity index 100% rename from .auto-resolution/docs/task.md rename to docs/task.md diff --git a/.auto-resolution/examples/__init__.py b/examples/__init__.py similarity index 100% rename from .auto-resolution/examples/__init__.py rename to examples/__init__.py diff --git a/.auto-resolution/examples/logging/__init__.py b/examples/logging/__init__.py similarity index 100% rename from .auto-resolution/examples/logging/__init__.py rename to examples/logging/__init__.py diff --git a/.auto-resolution/examples/logging/basic.log b/examples/logging/basic.log similarity index 100% rename from .auto-resolution/examples/logging/basic.log rename to examples/logging/basic.log diff --git a/.auto-resolution/examples/logging/basic.py b/examples/logging/basic.py similarity index 100% rename from .auto-resolution/examples/logging/basic.py rename to examples/logging/basic.py diff --git a/.auto-resolution/examples/logging/configure_logging.py b/examples/logging/configure_logging.py similarity index 100% rename from .auto-resolution/examples/logging/configure_logging.py rename to examples/logging/configure_logging.py diff --git a/.auto-resolution/examples/logging/fastapi.py b/examples/logging/fastapi.py similarity index 100% rename from .auto-resolution/examples/logging/fastapi.py rename to examples/logging/fastapi.py diff --git a/.auto-resolution/examples/simple_fastapi_app.py b/examples/simple_fastapi_app.py similarity index 100% rename from .auto-resolution/examples/simple_fastapi_app.py rename to examples/simple_fastapi_app.py diff --git a/.auto-resolution/examples/single_file_app.py b/examples/single_file_app.py similarity index 100% rename from .auto-resolution/examples/single_file_app.py rename to examples/single_file_app.py diff --git a/.auto-resolution/examples/sync/__init__.py b/examples/sync/__init__.py similarity index 100% rename from .auto-resolution/examples/sync/__init__.py rename to examples/sync/__init__.py diff --git a/.auto-resolution/examples/sync/leaderelection_anyio.py b/examples/sync/leaderelection_anyio.py similarity index 100% rename from .auto-resolution/examples/sync/leaderelection_anyio.py rename to examples/sync/leaderelection_anyio.py diff --git a/.auto-resolution/examples/sync/leaderelection_task.py b/examples/sync/leaderelection_task.py similarity index 100% rename from .auto-resolution/examples/sync/leaderelection_task.py rename to examples/sync/leaderelection_task.py diff --git a/.auto-resolution/examples/sync/lock.py b/examples/sync/lock.py similarity index 100% rename from .auto-resolution/examples/sync/lock.py rename to examples/sync/lock.py diff --git a/.auto-resolution/examples/sync/memory.py b/examples/sync/memory.py similarity index 100% rename from .auto-resolution/examples/sync/memory.py rename to examples/sync/memory.py diff --git a/.auto-resolution/examples/sync/postgres.py b/examples/sync/postgres.py similarity index 100% rename from .auto-resolution/examples/sync/postgres.py rename to examples/sync/postgres.py diff --git a/.auto-resolution/examples/sync/redis.py b/examples/sync/redis.py similarity index 100% rename from .auto-resolution/examples/sync/redis.py rename to examples/sync/redis.py diff --git a/.auto-resolution/examples/task/__init__.py b/examples/task/__init__.py similarity index 100% rename from .auto-resolution/examples/task/__init__.py rename to examples/task/__init__.py diff --git a/.auto-resolution/examples/task/fastapi.py b/examples/task/fastapi.py similarity index 100% rename from .auto-resolution/examples/task/fastapi.py rename to examples/task/fastapi.py diff --git a/.auto-resolution/examples/task/faststream.py b/examples/task/faststream.py similarity index 100% rename from .auto-resolution/examples/task/faststream.py rename to examples/task/faststream.py diff --git a/.auto-resolution/examples/task/interval_manager.py b/examples/task/interval_manager.py similarity index 100% rename from .auto-resolution/examples/task/interval_manager.py rename to examples/task/interval_manager.py diff --git a/.auto-resolution/examples/task/interval_router.py b/examples/task/interval_router.py similarity index 100% rename from .auto-resolution/examples/task/interval_router.py rename to examples/task/interval_router.py diff --git a/.auto-resolution/examples/task/leaderelection.py b/examples/task/leaderelection.py similarity index 100% rename from .auto-resolution/examples/task/leaderelection.py rename to examples/task/leaderelection.py diff --git a/.auto-resolution/examples/task/lock.py b/examples/task/lock.py similarity index 100% rename from .auto-resolution/examples/task/lock.py rename to examples/task/lock.py diff --git a/.auto-resolution/examples/task/router.py b/examples/task/router.py similarity index 100% rename from .auto-resolution/examples/task/router.py rename to examples/task/router.py diff --git a/.auto-resolution/grelmicro/__init__.py b/grelmicro/__init__.py similarity index 100% rename from .auto-resolution/grelmicro/__init__.py rename to grelmicro/__init__.py diff --git a/.auto-resolution/grelmicro/errors.py b/grelmicro/errors.py similarity index 100% rename from .auto-resolution/grelmicro/errors.py rename to grelmicro/errors.py diff --git a/.auto-resolution/grelmicro/logging/__init__.py b/grelmicro/logging/__init__.py similarity index 100% rename from .auto-resolution/grelmicro/logging/__init__.py rename to grelmicro/logging/__init__.py diff --git a/.auto-resolution/grelmicro/logging/config.py b/grelmicro/logging/config.py similarity index 100% rename from .auto-resolution/grelmicro/logging/config.py rename to grelmicro/logging/config.py diff --git a/.auto-resolution/grelmicro/logging/errors.py b/grelmicro/logging/errors.py similarity index 100% rename from .auto-resolution/grelmicro/logging/errors.py rename to grelmicro/logging/errors.py diff --git a/.auto-resolution/grelmicro/logging/loguru.py b/grelmicro/logging/loguru.py similarity index 100% rename from .auto-resolution/grelmicro/logging/loguru.py rename to grelmicro/logging/loguru.py diff --git a/.auto-resolution/grelmicro/py.typed b/grelmicro/py.typed similarity index 100% rename from .auto-resolution/grelmicro/py.typed rename to grelmicro/py.typed diff --git a/.auto-resolution/grelmicro/sync/__init__.py b/grelmicro/sync/__init__.py similarity index 100% rename from .auto-resolution/grelmicro/sync/__init__.py rename to grelmicro/sync/__init__.py diff --git a/.auto-resolution/grelmicro/sync/_backends.py b/grelmicro/sync/_backends.py similarity index 100% rename from .auto-resolution/grelmicro/sync/_backends.py rename to grelmicro/sync/_backends.py diff --git a/.auto-resolution/grelmicro/sync/_base.py b/grelmicro/sync/_base.py similarity index 100% rename from .auto-resolution/grelmicro/sync/_base.py rename to grelmicro/sync/_base.py diff --git a/.auto-resolution/grelmicro/sync/_utils.py b/grelmicro/sync/_utils.py similarity index 100% rename from .auto-resolution/grelmicro/sync/_utils.py rename to grelmicro/sync/_utils.py diff --git a/.auto-resolution/grelmicro/sync/abc.py b/grelmicro/sync/abc.py similarity index 100% rename from .auto-resolution/grelmicro/sync/abc.py rename to grelmicro/sync/abc.py diff --git a/.auto-resolution/grelmicro/sync/errors.py b/grelmicro/sync/errors.py similarity index 100% rename from .auto-resolution/grelmicro/sync/errors.py rename to grelmicro/sync/errors.py diff --git a/.auto-resolution/grelmicro/sync/leaderelection.py b/grelmicro/sync/leaderelection.py similarity index 100% rename from .auto-resolution/grelmicro/sync/leaderelection.py rename to grelmicro/sync/leaderelection.py diff --git a/.auto-resolution/grelmicro/sync/lock.py b/grelmicro/sync/lock.py similarity index 100% rename from .auto-resolution/grelmicro/sync/lock.py rename to grelmicro/sync/lock.py diff --git a/.auto-resolution/grelmicro/sync/memory.py b/grelmicro/sync/memory.py similarity index 100% rename from .auto-resolution/grelmicro/sync/memory.py rename to grelmicro/sync/memory.py diff --git a/.auto-resolution/grelmicro/sync/postgres.py b/grelmicro/sync/postgres.py similarity index 100% rename from .auto-resolution/grelmicro/sync/postgres.py rename to grelmicro/sync/postgres.py diff --git a/.auto-resolution/grelmicro/sync/redis.py b/grelmicro/sync/redis.py similarity index 100% rename from .auto-resolution/grelmicro/sync/redis.py rename to grelmicro/sync/redis.py diff --git a/.auto-resolution/grelmicro/task/__init__.py b/grelmicro/task/__init__.py similarity index 100% rename from .auto-resolution/grelmicro/task/__init__.py rename to grelmicro/task/__init__.py diff --git a/.auto-resolution/grelmicro/task/_interval.py b/grelmicro/task/_interval.py similarity index 100% rename from .auto-resolution/grelmicro/task/_interval.py rename to grelmicro/task/_interval.py diff --git a/.auto-resolution/grelmicro/task/_utils.py b/grelmicro/task/_utils.py similarity index 100% rename from .auto-resolution/grelmicro/task/_utils.py rename to grelmicro/task/_utils.py diff --git a/.auto-resolution/grelmicro/task/abc.py b/grelmicro/task/abc.py similarity index 100% rename from .auto-resolution/grelmicro/task/abc.py rename to grelmicro/task/abc.py diff --git a/.auto-resolution/grelmicro/task/errors.py b/grelmicro/task/errors.py similarity index 100% rename from .auto-resolution/grelmicro/task/errors.py rename to grelmicro/task/errors.py diff --git a/.auto-resolution/grelmicro/task/manager.py b/grelmicro/task/manager.py similarity index 100% rename from .auto-resolution/grelmicro/task/manager.py rename to grelmicro/task/manager.py diff --git a/.auto-resolution/grelmicro/task/router.py b/grelmicro/task/router.py similarity index 100% rename from .auto-resolution/grelmicro/task/router.py rename to grelmicro/task/router.py diff --git a/.auto-resolution/mkdocs.yml b/mkdocs.yml similarity index 100% rename from .auto-resolution/mkdocs.yml rename to mkdocs.yml diff --git a/.auto-resolution/pyproject.toml b/pyproject.toml similarity index 100% rename from .auto-resolution/pyproject.toml rename to pyproject.toml diff --git a/.auto-resolution/tests/__init__.py b/tests/__init__.py similarity index 100% rename from .auto-resolution/tests/__init__.py rename to tests/__init__.py diff --git a/.auto-resolution/tests/conftest.py b/tests/conftest.py similarity index 100% rename from .auto-resolution/tests/conftest.py rename to tests/conftest.py diff --git a/.auto-resolution/tests/logging/__init__.py b/tests/logging/__init__.py similarity index 100% rename from .auto-resolution/tests/logging/__init__.py rename to tests/logging/__init__.py diff --git a/.auto-resolution/tests/logging/test_loguru.py b/tests/logging/test_loguru.py similarity index 100% rename from .auto-resolution/tests/logging/test_loguru.py rename to tests/logging/test_loguru.py diff --git a/.auto-resolution/tests/sync/__init__.py b/tests/sync/__init__.py similarity index 100% rename from .auto-resolution/tests/sync/__init__.py rename to tests/sync/__init__.py diff --git a/.auto-resolution/tests/sync/test_backends.py b/tests/sync/test_backends.py similarity index 100% rename from .auto-resolution/tests/sync/test_backends.py rename to tests/sync/test_backends.py diff --git a/.auto-resolution/tests/sync/test_leaderelection.py b/tests/sync/test_leaderelection.py similarity index 100% rename from .auto-resolution/tests/sync/test_leaderelection.py rename to tests/sync/test_leaderelection.py diff --git a/.auto-resolution/tests/sync/test_lock.py b/tests/sync/test_lock.py similarity index 100% rename from .auto-resolution/tests/sync/test_lock.py rename to tests/sync/test_lock.py diff --git a/.auto-resolution/tests/sync/test_postgres.py b/tests/sync/test_postgres.py similarity index 87% rename from .auto-resolution/tests/sync/test_postgres.py rename to tests/sync/test_postgres.py index ef8dd18..dc933b8 100644 --- a/.auto-resolution/tests/sync/test_postgres.py +++ b/tests/sync/test_postgres.py @@ -8,7 +8,7 @@ pytestmark = [pytest.mark.anyio, pytest.mark.timeout(1)] -URL = "postgres://user:password@localhost:5432/db" +URL = "postgresql://test_user:test_password@test_host:1234/test_db" @pytest.mark.parametrize( @@ -51,9 +51,7 @@ async def test_sync_backend_out_of_context_errors() -> None: @pytest.mark.parametrize( ("environs"), [ - { - "POSTGRES_URL": "postgresql://test_user:test_password@test_host:1234/test_db" - }, + {"POSTGRES_URL": URL}, { "POSTGRES_USER": "test_user", "POSTGRES_PASSWORD": "test_password", @@ -75,10 +73,7 @@ def test_postgres_env_var_settings( backend = PostgresSyncBackend() # Assert - assert ( - backend._url - == "postgresql://test_user:test_password@test_host:1234/test_db" - ) + assert backend._url == URL @pytest.mark.parametrize( @@ -88,6 +83,14 @@ def test_postgres_env_var_settings( "POSTGRES_URL": "test://test_user:test_password@test_host:1234/test_db" }, {"POSTGRES_USER": "test_user"}, + { + "POSTGRES_URL": URL, + "POSTGRES_USER": "test_user", + "POSTGRES_PASSWORD": "test_password", + "POSTGRES_HOST": "test_host", + "POSTGRES_PORT": "1234", + "POSTGRES_DB": "test_db", + }, ], ) def test_postgres_env_var_settings_validation_error( diff --git a/.auto-resolution/tests/sync/test_redis.py b/tests/sync/test_redis.py similarity index 100% rename from .auto-resolution/tests/sync/test_redis.py rename to tests/sync/test_redis.py diff --git a/.auto-resolution/tests/sync/utils.py b/tests/sync/utils.py similarity index 100% rename from .auto-resolution/tests/sync/utils.py rename to tests/sync/utils.py diff --git a/.auto-resolution/tests/task/__init__.py b/tests/task/__init__.py similarity index 100% rename from .auto-resolution/tests/task/__init__.py rename to tests/task/__init__.py diff --git a/.auto-resolution/tests/task/samples.py b/tests/task/samples.py similarity index 100% rename from .auto-resolution/tests/task/samples.py rename to tests/task/samples.py diff --git a/.auto-resolution/tests/task/test_interval.py b/tests/task/test_interval.py similarity index 100% rename from .auto-resolution/tests/task/test_interval.py rename to tests/task/test_interval.py diff --git a/.auto-resolution/tests/task/test_manager.py b/tests/task/test_manager.py similarity index 100% rename from .auto-resolution/tests/task/test_manager.py rename to tests/task/test_manager.py diff --git a/.auto-resolution/tests/task/test_router.py b/tests/task/test_router.py similarity index 100% rename from .auto-resolution/tests/task/test_router.py rename to tests/task/test_router.py diff --git a/.auto-resolution/uv.lock b/uv.lock similarity index 100% rename from .auto-resolution/uv.lock rename to uv.lock From 7baf69b4227418bb499adeca2c4edd4ebcb7812f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sat, 30 Nov 2024 14:10:26 +0100 Subject: [PATCH 12/15] =?UTF-8?q?=F0=9F=8E=A8=20Refactor=20pytest=20config?= =?UTF-8?q?uration=20for=20unit=20and=20integration=20testing,=20and=20add?= =?UTF-8?q?=20coverage=20report=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 23 +++++++++++++++++++---- .vscode/settings.json | 2 +- pyproject.toml | 2 -- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e5a141..c944d92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,14 +50,29 @@ repos: types: [python] require_serial: true - - id: pytest - name: pytest + - id: pytest-unit + name: pytest-unit description: "Run 'pytest' for unit testing" - entry: uv run pytest --cov-fail-under=90 + entry: uv run pytest -m "not integration" language: system pass_filenames: false + - id: pytest-integration + name: pytest-integration + description: "Run 'pytest' for integration testing" + entry: uv run pytest -m "integration" --cov-append + language: system + pass_filenames: false + + - id: coverage-report + name: coverage-report + description: "Generate coverage report" + entry: uv run coverage report --fail-under=100 + language: system + pass_filenames: false + + ci: autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate - skip: [uv-lock, mypy, pytest] + skip: [uv-lock, mypy, pytest-unit, pytest-integration] diff --git a/.vscode/settings.json b/.vscode/settings.json index 806ffc4..01e3b38 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,7 +23,7 @@ "python.terminal.activateEnvironment": true, "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, - "python.testing.pytestArgs": ["--no-cov", "--color=yes"], + "python.testing.pytestArgs": ["--color=yes"], "python.analysis.inlayHints.pytestParameters": true, // Python editor settings diff --git a/pyproject.toml b/pyproject.toml index 9bcca87..16cf486 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -153,11 +153,9 @@ disallow_untyped_defs = false [tool.pytest.ini_options] addopts = """ --cov=grelmicro - --cov-report term:skip-covered --cov-report xml:cov.xml --strict-config --strict-markers - -m "not integration" """ markers = """ integration: mark a test as an integration test (disabled by default). From adf7a2d2ed8c486741c82d635c0ce00ef24aa5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sat, 30 Nov 2024 14:10:40 +0100 Subject: [PATCH 13/15] =?UTF-8?q?=E2=9C=A8=20Add=20prefix=20support=20for?= =?UTF-8?q?=20Redis=20keys=20to=20avoid=20conflicts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grelmicro/sync/redis.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/grelmicro/sync/redis.py b/grelmicro/sync/redis.py index 73090c8..b763f23 100644 --- a/grelmicro/sync/redis.py +++ b/grelmicro/sync/redis.py @@ -87,6 +87,14 @@ def __init__( """), ] = None, *, + prefix: Annotated[ + str, + Doc(""" + The prefix to add on redis keys to avoid conflicts with other keys. + + By default no prefix is added. + """), + ] = "", auto_register: Annotated[ bool, Doc( @@ -97,6 +105,7 @@ def __init__( """Initialize the lock backend.""" self._url = url or _get_redis_url() self._redis: Redis = Redis.from_url(str(self._url)) + self._prefix = prefix self._lua_release = self._redis.register_script(self._LUA_RELEASE) self._lua_acquire = self._redis.register_script( self._LUA_ACQUIRE_OR_EXTEND @@ -121,7 +130,7 @@ async def acquire(self, *, name: str, token: str, duration: float) -> bool: """Acquire the lock.""" return bool( await self._lua_acquire( - keys=[name], + keys=[f"{self._prefix}{name}"], args=[token, int(duration * 1000)], client=self._redis, ) @@ -131,16 +140,16 @@ async def release(self, *, name: str, token: str) -> bool: """Release the lock.""" return bool( await self._lua_release( - keys=[name], args=[token], client=self._redis + keys=[f"{self._prefix}{name}"], args=[token], client=self._redis ) ) async def locked(self, *, name: str) -> bool: """Check if the lock is acquired.""" - return bool(await self._redis.get(name)) + return bool(await self._redis.get(f"{self._prefix}{name}")) async def owned(self, *, name: str, token: str) -> bool: """Check if the lock is owned.""" return bool( - (await self._redis.get(name)) == token.encode() + (await self._redis.get(f"{self._prefix}{name}")) == token.encode() ) # redis returns bytes From 50ede0be1ee9297029ea24c725d736394be72083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sat, 30 Nov 2024 14:17:58 +0100 Subject: [PATCH 14/15] =?UTF-8?q?=F0=9F=92=9A=20Update=20pre-commit=20conf?= =?UTF-8?q?iguration=20to=20skip=20coverage=20report=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c944d92..6fb9a26 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,4 +75,4 @@ repos: ci: autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate - skip: [uv-lock, mypy, pytest-unit, pytest-integration] + skip: [uv-lock, mypy, pytest-unit, pytest-integration, coverage-report] From 5a9f3fff95700b28acd27a04da76c236c1b7e583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Wed, 4 Dec 2024 06:26:38 +0100 Subject: [PATCH 15/15] =?UTF-8?q?=F0=9F=91=B7=20Set=20UV=5FFROZEN=20enviro?= =?UTF-8?q?nment=20variable=20in=20CI=20and=20release=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 3 +++ .github/workflows/release.yml | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fb99bb..6d54854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: ["main"] +env: + UV_FROZEN: 1 + jobs: lint: name: Lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8d4bab..dca6916 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,6 +66,8 @@ jobs: publish-docs: runs-on: ubuntu-latest needs: [bump-version] + env: + UV_FROZEN: 1 steps: - uses: actions/checkout@v4 with: @@ -91,6 +93,8 @@ jobs: publish-pypi: needs: [bump-version] runs-on: ubuntu-latest + env: + UV_FROZEN: 1 steps: - name: Checkout uses: actions/checkout@v4