From 195036df1eb58f865c23deb94477062607d235de Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 15:55:02 +0100 Subject: [PATCH 01/22] Update dependencies --- requirements.txt | 7 ++++--- requirements/dev.txt | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index fc57e72..6360346 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -psycopg2>=2.4.5 -Flask>=0.10 -typing +psycopg2>=2.9.11 +Flask>=3.1.2 +cachelib>=0.13.0 +setuptools>=80.9.0 \ No newline at end of file diff --git a/requirements/dev.txt b/requirements/dev.txt index dc7ebe2..7a9de80 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,5 +1,4 @@ -r ../requirements.txt flake8 flake8-import-order -coverage -mock +coverage \ No newline at end of file From 144b90b757fba03315610c1c1b2045ee316a68dd Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 15:59:14 +0100 Subject: [PATCH 02/22] Update BaseBackend class --- flask_phpbb3/backends/base.py | 53 +++++++++++++++++------------------ 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/flask_phpbb3/backends/base.py b/flask_phpbb3/backends/base.py index 123ea1d..4435261 100644 --- a/flask_phpbb3/backends/base.py +++ b/flask_phpbb3/backends/base.py @@ -1,13 +1,13 @@ -from __future__ import absolute_import +import abc +from typing import Any, Dict, Optional -import typing +import cachelib -import werkzeug.contrib.cache -ACL_OPTIONS_CACHE_TTL = 3600 * 1 +ACL_OPTIONS_CACHE_TTL: int = 3600 * 1 -class BaseBackend(object): +class BaseBackend: KNOWN_OPERATIONS = ( 'fetch', 'get', @@ -20,11 +20,10 @@ class BaseBackend(object): def __init__( self, - cache, # type: werkzeug.contrib.cache.BaseCache - config # type: typing.Dict[str, str] + cache: cachelib.BaseCache, + config: Dict[str, Any] ): - # type: (...) -> None - self._functions = {} # type: typing.Dict[str, str] + self._functions: Dict[str, str] = {} self._connection = None self._cache = cache self._config = config @@ -35,47 +34,45 @@ def __init__( custom_statements = {} self._functions.update(custom_statements) - def _setup_connection(self): - # type: () -> None + @abc.abstractmethod + def _setup_connection(self) -> None: raise NotImplementedError - def _prepare_statements(self): - # type: () -> None + @abc.abstractmethod + def _prepare_statements(self) -> None: raise NotImplementedError @property - def _db(self): - # type: () -> typing.Any + @abc.abstractmethod + def _db(self) -> Any: raise NotImplementedError + @abc.abstractmethod def execute( self, - command, # type: str - cache=False, # type: bool - cache_ttl=None, # type: typing.Optional[int] - **kwargs # type: typing.Any - ): - # type: (...) -> typing.Any + command: str, + cache: bool = False, + cache_ttl: Optional[int] = None, + **kwargs + ) -> Any: raise NotImplementedError - def close(self): - # type: () -> None + @abc.abstractmethod + def close(self) -> None: raise NotImplementedError @property - def is_closed(self): - # type: () -> bool + @abc.abstractmethod + def is_closed(self) -> bool: raise NotImplementedError - def get_user_acl(self, raw_user_permissions): - # type: (str) -> UserAcl + def get_user_acl(self, raw_user_permissions: str) -> UserAcl: raw_acl_options = self.execute( 'fetch_acl_options', cache=True, cache_ttl=ACL_OPTIONS_CACHE_TTL, limit=None, ) - return UserAcl(raw_acl_options, raw_user_permissions) From 4f2dc48e09551e0aa456619988910ed9fdabe354 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 17:34:41 +0100 Subject: [PATCH 03/22] Update UserAcl --- flask_phpbb3/backends/base.py | 41 ++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/flask_phpbb3/backends/base.py b/flask_phpbb3/backends/base.py index 4435261..42f90ac 100644 --- a/flask_phpbb3/backends/base.py +++ b/flask_phpbb3/backends/base.py @@ -1,5 +1,5 @@ import abc -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional import cachelib @@ -77,22 +77,23 @@ def get_user_acl(self, raw_user_permissions: str) -> UserAcl: class UserAcl(object): - def __init__(self, raw_acl_options, raw_user_permissions): - # type: (typing.List[dict], str) -> None + def __init__(self, raw_acl_options: List[Dict], raw_user_permissions: str): self._acl_options = self._parse_acl_options(raw_acl_options) self._acl = self._parse_user_permissions(raw_user_permissions) - self._acl_lookup_cache = {} # type: dict + self._acl_lookup_cache: Dict[str, Any] = {} @classmethod - def _parse_acl_options(cls, raw_acl_options): - # type: (typing.List[dict]) -> dict + def _parse_acl_options( + cls, + raw_acl_options: List[Dict[str, Any]] + ) -> Dict[str, Dict[str, int]]: # Load ACL options, so we can decode the user ACL - acl_options = { + acl_options: Dict[str, Dict[str, int]] = { 'local': {}, 'global': {} - } # type: dict - local_index = 0 - global_index = 0 + } + local_index: int = 0 + global_index: int = 0 for opt in raw_acl_options or []: if opt['is_local'] == 1: @@ -109,10 +110,12 @@ def _parse_acl_options(cls, raw_acl_options): return acl_options @classmethod - def _parse_user_permissions(cls, raw_user_permissions): - # type: (str) -> dict - seq_cache = {} # type: dict - acl = {} + def _parse_user_permissions( + cls, + raw_user_permissions: str + ) -> Dict[str, str]: + seq_cache: Dict[str, str] = {} + acl: Dict[str, str] = {} split_user_permissions = raw_user_permissions\ .rstrip()\ @@ -136,8 +139,7 @@ def _parse_user_permissions(cls, raw_user_permissions): return acl - def has_privilege(self, privilege, forum_id=0): - # type: (str, int) -> bool + def has_privilege(self, privilege: str, forum_id: int = 0) -> bool: # Parse negation str_forum_id = str(forum_id) @@ -172,13 +174,12 @@ def has_privilege(self, privilege, forum_id=0): except IndexError: pass - output = ( + output: bool = ( negated ^ self._acl_lookup_cache[str_forum_id][option] - ) # type: bool + ) return output - def has_privileges(self, *privileges, **kwargs): - # type: (*str, **int) -> bool + def has_privileges(self, *privileges: str, **kwargs: int) -> bool: forum_id = kwargs.get('forum_id', 0) output = False From 71249f754e834f95221384a6f88e130e70dabf84 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 17:35:16 +0100 Subject: [PATCH 04/22] Update UserAcl --- flask_phpbb3/backends/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask_phpbb3/backends/base.py b/flask_phpbb3/backends/base.py index 42f90ac..e934cf0 100644 --- a/flask_phpbb3/backends/base.py +++ b/flask_phpbb3/backends/base.py @@ -76,7 +76,7 @@ def get_user_acl(self, raw_user_permissions: str) -> UserAcl: return UserAcl(raw_acl_options, raw_user_permissions) -class UserAcl(object): +class UserAcl: def __init__(self, raw_acl_options: List[Dict], raw_user_permissions: str): self._acl_options = self._parse_acl_options(raw_acl_options) self._acl = self._parse_user_permissions(raw_user_permissions) From 2e22b85078eef0c3b7574b9a9f9abe8dde8ef6ff Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 17:36:47 +0100 Subject: [PATCH 05/22] Update Python version in mypy config --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index b5d1425..cc00d9c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,5 @@ [mypy] -python_version = 2.7 +python_version = 3.14 warn_return_any = True warn_unused_configs = True disallow_untyped_defs = True From 4a98464db9933c3f7f3b9d39511540fd59d9d532 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 18:19:04 +0100 Subject: [PATCH 06/22] Modify BaseBackend.execute method to improve typing --- flask_phpbb3/backends/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask_phpbb3/backends/base.py b/flask_phpbb3/backends/base.py index e934cf0..5403cb4 100644 --- a/flask_phpbb3/backends/base.py +++ b/flask_phpbb3/backends/base.py @@ -53,7 +53,9 @@ def execute( command: str, cache: bool = False, cache_ttl: Optional[int] = None, - **kwargs + skip: int = 0, + limit: Optional[int] = 10, + **kwargs: int | str ) -> Any: raise NotImplementedError From 0dd3ff00be5f91ea9fc384271cc6caad78239615 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 18:20:01 +0100 Subject: [PATCH 07/22] Update Psycopg2Backend --- flask_phpbb3/backends/psycopg2.py | 75 +++++++++++++++---------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/flask_phpbb3/backends/psycopg2.py b/flask_phpbb3/backends/psycopg2.py index 816e990..70b0b53 100644 --- a/flask_phpbb3/backends/psycopg2.py +++ b/flask_phpbb3/backends/psycopg2.py @@ -1,7 +1,7 @@ from __future__ import absolute_import import json -import typing +from typing import Any, Dict, List, Optional try: import psycopg2 @@ -15,8 +15,7 @@ class Psycopg2Backend(base.BaseBackend): - def _setup_connection(self): - # type: () -> None + def _setup_connection(self) -> None: self._connection = psycopg2.connect( 'dbname={DATABASE}' ' host={HOST}' @@ -26,15 +25,13 @@ def _setup_connection(self): ) @property - def _db(self): - # type: () -> psycopg2.extensions.connection + def _db(self) -> psycopg2.extensions.connection: if not self._connection: self._setup_connection() return self._connection - def _prepare_statements(self): - # type: () -> None + def _prepare_statements(self) -> None: """ Initializes prepared SQL statements, depending on version of PHPBB3 """ @@ -106,13 +103,12 @@ def _prepare_statements(self): # TODO Add/Move to version specific queries - def _prepare_custom_fields_statements(self): - # type: () -> None + def _prepare_custom_fields_statements(self) -> None: """ Prepares statements for custom fields """ # Setters for custom fields - custom_fields = self._config.get('CUSTOM_USER_FIELDS', []) + custom_fields: List[Any] = self._config.get('CUSTOM_USER_FIELDS', []) for custom_field in custom_fields: self._functions["set_{0}".format(custom_field)] = ( "UPDATE" @@ -128,15 +124,14 @@ def _prepare_custom_fields_statements(self): def _sql_query( self, - operation, # type: str - query, # type: str - cache_key_prefix=None, # type: typing.Optional[str] - cache_ttl=None, # type: typing.Optional[int] - skip=0, # type: int - limit=10, # type: typing.Optional[int] - **kwargs # type: typing.Union[int, str] - ): - # type: (...) -> typing.Any + operation: str, + query: str, + cache_key_prefix: Optional[str] = None, + cache_ttl: Optional[int] = None, + skip: int = 0, + limit: Optional[int] = 10, + **kwargs: int | str + ) -> Any: """Executes a query with values in kwargs.""" if operation not in self.KNOWN_OPERATIONS: raise ValueError("Unknown operation") @@ -149,7 +144,7 @@ def _sql_query( for key, value in kwargs.items()) ) raw_data = self._cache.get(cache_key) - if raw_data and isinstance(raw_data, (str, unicode)): + if raw_data and isinstance(raw_data, (str, bytes)): try: return json.loads(raw_data) except ValueError: @@ -172,8 +167,12 @@ def _sql_query( return output - def _paginate_query(self, query, skip, limit): - # type: (str, int, typing.Optional[int]) -> str + def _paginate_query( + self, + query: str, + skip: int, + limit: Optional[int] + ) -> str: output = query + ' OFFSET {:d}'.format(skip) if limit: output += ' LIMIT {:d}'.format(limit) @@ -181,11 +180,10 @@ def _paginate_query(self, query, skip, limit): def _execute_operation( self, - operation, # type: str - query, # type: str - params # type: typing.Dict[str, typing.Union[str, int]] - ): - # type: (...) -> typing.Any + operation: str, + query: str, + params: Dict[str, str | int] + ) -> Any: cursor = self._db.cursor() cursor.execute( @@ -213,15 +211,14 @@ def _execute_operation( def execute( self, - command, # type: str - cache=False, # type: bool - cache_ttl=None, # type: typing.Optional[int] - skip=0, # type: int - limit=10, # type: typing.Optional[int] - **kwargs # type: typing.Union[int, str] - ): - # type: (...) -> typing.Any - cache_key_prefix = None + command: str, + cache: bool = False, + cache_ttl: Optional[int] = None, + skip: int = 0, + limit: Optional[int] = 10, + **kwargs: int | str + ) -> Any: + cache_key_prefix: Optional[str] = None if cache: cache_key_prefix = command @@ -246,11 +243,9 @@ def execute( **kwargs ) - def close(self): - # type: () -> None + def close(self) -> None: self._db.close() @property - def is_closed(self): - # type: () -> bool + def is_closed(self) -> bool: return bool(self._db.closed) From fb2e7b708009ddceabee1725d14081ee94bd37e7 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 18:24:12 +0100 Subject: [PATCH 08/22] Remove absolute_import --- flask_phpbb3/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flask_phpbb3/__init__.py b/flask_phpbb3/__init__.py index 2fbfbf7..86d99e0 100644 --- a/flask_phpbb3/__init__.py +++ b/flask_phpbb3/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import pkg_resources __version__ = pkg_resources.get_distribution(__name__).version From fb9a1f4dc52ef9f6f7917054532a109fc2903ce8 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 18:25:36 +0100 Subject: [PATCH 09/22] Bump Python version in configs --- .travis.yml | 8 ++++---- setup.cfg | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13b6f09..a3d69f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,12 +8,12 @@ python: - "3.6" install: - | - if [[ $TRAVIS_PYTHON_VERSION = 2.7 ]]; then + if [[ $TRAVIS_PYTHON_VERSION = 3.14 ]]; then pip install -r requirements/dev.txt else pip install -r requirements/dev-3.txt fi script: - - if [[ $TRAVIS_PYTHON_VERSION = 2.7 ]]; then flake8; fi - - if [[ $TRAVIS_PYTHON_VERSION != 2.7 ]]; then mypy flask_phpbb3; fi - - if [[ $TRAVIS_PYTHON_VERSION = 2.7 ]]; then python setup.py test; fi + - if [[ $TRAVIS_PYTHON_VERSION = 3.14 ]]; then flake8; fi + - if [[ $TRAVIS_PYTHON_VERSION != 3.14 ]]; then mypy flask_phpbb3; fi + - if [[ $TRAVIS_PYTHON_VERSION = 3.14 ]]; then python setup.py test; fi diff --git a/setup.cfg b/setup.cfg index c9a3482..173d176 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,8 +12,8 @@ classifier = License :: OSI Approved :: BSD License Operating System :: OS Independent Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.14 Topic :: Internet :: WWW/HTTP :: Dynamic Content Topic :: Software Development :: Libraries :: Python Modules From 68ef44ff17b854efab830cc074c179b95f1c7868 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 18:27:08 +0100 Subject: [PATCH 10/22] Update imports in psycopg2.py, extensions.py, and sessions.py --- flask_phpbb3/backends/psycopg2.py | 2 -- flask_phpbb3/extension.py | 4 +--- flask_phpbb3/sessions.py | 4 +--- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/flask_phpbb3/backends/psycopg2.py b/flask_phpbb3/backends/psycopg2.py index 70b0b53..a7a37ec 100644 --- a/flask_phpbb3/backends/psycopg2.py +++ b/flask_phpbb3/backends/psycopg2.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import json from typing import Any, Dict, List, Optional diff --git a/flask_phpbb3/extension.py b/flask_phpbb3/extension.py index 63560ea..a0e69f2 100644 --- a/flask_phpbb3/extension.py +++ b/flask_phpbb3/extension.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import typing import flask @@ -7,7 +5,7 @@ import flask_phpbb3.backends.base import flask_phpbb3.sessions -import werkzeug.contrib.cache +import cachelib class PhpBB3(object): diff --git a/flask_phpbb3/sessions.py b/flask_phpbb3/sessions.py index 61ee452..436d25f 100644 --- a/flask_phpbb3/sessions.py +++ b/flask_phpbb3/sessions.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import json import typing @@ -10,7 +8,7 @@ import flask_phpbb3 import flask_phpbb3.backends.base -import werkzeug.contrib.cache +import cachelib ANONYMOUS_CACHE_TTL = 3600 * 24 From 2a3e0d8341ac2ab9a3f41bd8060aa5d9b07ff162 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 19:05:23 +0100 Subject: [PATCH 11/22] Update PhpBB3, reassign phpbb3_backend from flask._app_ctx_stack to flask.g --- flask_phpbb3/extension.py | 209 ++++++++++++++++++++------------------ 1 file changed, 108 insertions(+), 101 deletions(-) diff --git a/flask_phpbb3/extension.py b/flask_phpbb3/extension.py index a0e69f2..03ca8c0 100644 --- a/flask_phpbb3/extension.py +++ b/flask_phpbb3/extension.py @@ -1,44 +1,43 @@ -import typing +from typing import Any, Dict, List, Optional + +import cachelib import flask import flask_phpbb3.backends.base import flask_phpbb3.sessions -import cachelib - -class PhpBB3(object): +class PhpBB3: def __init__( self, - app=None, # type: typing.Optional[flask.Flask] - cache=None, # type: werkzeug.contrib.cache.BaseCache - ): - # type: (...) -> None + app: Optional[flask.Flask] = None, + cache: Optional[cachelib.BaseCache] = None, + ) -> None: self.app = app if app is not None: self.init_app(app, cache) def init_app( self, - app, # type: flask.Flask - cache=None # type: typing.Optional[werkzeug.contrib.cache.BaseCache] - ): - # type: (...) -> None + app: flask.Flask, + cache: Optional[cachelib.BaseCache] = None + ) -> None: self._ensure_default_config(app) # Use passed in cache interface (see Flask-Cache extension) + cache_driver: cachelib.BaseCache if not cache: # Setup our own cache_backend = app.config['PHPBB3_SESSION_BACKEND']['TYPE'] if cache_backend == 'memcached': key_prefix = app.config['PHPBB3_SESSION_BACKEND']['KEY_PREFIX'] - cache_driver = werkzeug.contrib.cache.MemcachedCache( + cache_driver = cachelib.MemcachedCache( app.config['PHPBB3_SESSION_BACKEND']['SERVERS'], key_prefix=key_prefix, - ) # type: werkzeug.contrib.cache.BaseCache + ) else: - cache_driver = werkzeug.contrib.cache.SimpleCache() + cache_driver = cachelib.SimpleCache() else: cache_driver = cache @@ -46,8 +45,8 @@ def init_app( app.teardown_appcontext(self.teardown) # Add ourselves to the app, so session interface can function - app.phpbb3 = self - app.phpbb3_cache = cache_driver + setattr(app, 'phpbb3', self) + setattr(app, 'phpbb3_cache', cache_driver) # Use our session interface # TODO Is it wise to do it here? Should user do it himself? @@ -55,8 +54,7 @@ def init_app( flask_phpbb3.sessions.PhpBB3SessionInterface() @classmethod - def _ensure_default_config(cls, app): - # type: (flask.Flask) -> None + def _ensure_default_config(cls, app: flask.Flask) -> None: app.config.setdefault('PHPBB3', {}) app.config['PHPBB3'].setdefault('DRIVER', 'psycopg2') app.config['PHPBB3'].setdefault('VERSION', '3.1') @@ -86,11 +84,10 @@ def _ensure_default_config(cls, app): @classmethod def _create_backend( cls, - backend_type, # type: str - config, # type: dict - cache # type: werkzeug.contrib.cache.BaseCache - ): - # type: (...) -> flask_phpbb3.backends.base.BaseBackend + backend_type: str, + config: Dict[str, Any], + cache: cachelib.BaseCache + ) -> flask_phpbb3.backends.base.BaseBackend: if backend_type == 'psycopg2': import flask_phpbb3.backends.psycopg2 return flask_phpbb3.backends.psycopg2.Psycopg2Backend( @@ -101,154 +98,164 @@ def _create_backend( raise ValueError('Unsupported driver {}'.format(backend_type)) @property - def _backend(self): - # type: () -> flask_phpbb3.backends.base.BaseBackend + def _backend(self) -> flask_phpbb3.backends.base.BaseBackend: """Returns phpbb3 backend""" current_app = self.app or flask.current_app - ctx = flask._app_ctx_stack.top - if ctx is not None: - if not hasattr(ctx, 'phpbb3_backend')\ - or ctx.phpbb3_backend.is_closed: - backend = PhpBB3._create_backend( - current_app.config['PHPBB3']['DRIVER'], - current_app.config['PHPBB3_DATABASE'], - current_app.phpbb3_cache, - ) - ctx.phpbb3_backend = backend - else: - backend = ctx.phpbb3_backend - return backend - raise AttributeError('No context available') + backend: Optional[flask_phpbb3.backends.base.BaseBackend] =\ + flask.g.get('phpbb3_backend', None) - def get_autologin(self, key, cache=False, cache_ttl=None): - # type: (str, bool, typing.Optional[int]) -> typing.Optional[dict] - output = self._backend.execute( + if backend is None or backend.is_closed: + cache = getattr(current_app, 'phpbb3_cache') + backend = PhpBB3._create_backend( + current_app.config['PHPBB3']['DRIVER'], + current_app.config['PHPBB3_DATABASE'], + cache, + ) + flask.g.setdefault('phpbb3_backend', backend) + return backend + + def get_autologin( + self, + key: str, + cache: bool = False, + cache_ttl: Optional[int] = None + ) -> Optional[Dict]: + output: Optional[Dict] = self._backend.execute( 'get_autologin', key=key, cache=cache, cache_ttl=cache_ttl, - ) # type: typing.Optional[dict] + ) return output - def get_session(self, session_id, cache=False, cache_ttl=None): - # type: (str, bool, typing.Optional[int]) -> typing.Optional[dict] - output = self._backend.execute( + def get_session( + self, + session_id: str, + cache: bool = False, + cache_ttl: Optional[int] = None + ) -> Optional[Dict]: + output: Optional[Dict] = self._backend.execute( 'get_session', session_id=session_id, cache=cache, cache_ttl=cache_ttl, - ) # type: typing.Optional[dict] + ) return output - def get_user(self, user_id, cache=False, cache_ttl=None): - # type: (int, bool, typing.Optional[int]) -> typing.Optional[dict] - output = self._backend.execute( + def get_user( + self, + user_id: int, + cache: bool = False, + cache_ttl: Optional[int] = None + ) -> Optional[Dict]: + output: Optional[Dict] = self._backend.execute( 'get_user', user_id=user_id, cache=cache, cache_ttl=cache_ttl, - ) # type: typing.Optional[dict] + ) return output - def get_user_profile(self, user_id, cache=False, cache_ttl=None): - # type: (int, bool, typing.Optional[int]) -> typing.Optional[dict] - output = self._backend.execute( + def get_user_profile( + self, + user_id: int, + cache: bool = False, + cache_ttl: Optional[int] = None + ) -> Optional[Dict]: + output: Optional[Dict] = self._backend.execute( 'get_user_profile', user_id=user_id, cache=cache, cache_ttl=cache_ttl, - ) # type: typing.Optional[dict] + ) return output def has_membership( self, - user_id, # type: int - group_id, # type: int - cache=False, # type: bool - cache_ttl=None, # type: typing.Optional[int] - ): - # type: (...) -> typing.Optional[bool] - output = self._backend.execute( + user_id: int, + group_id: int, + cache: bool = False, + cache_ttl: Optional[int] = None, + ) -> Optional[bool]: + output: Optional[bool] = self._backend.execute( 'has_membership', user_id=user_id, group_id=group_id, cache=cache, cache_ttl=cache_ttl, - ) # type: typing.Optional[bool] + ) return output def has_membership_resolve( self, - user_id, # type: int - group_name, # type: str - cache=False, # type: bool - cache_ttl=None, # type: typing.Optional[int] - ): - # type: (...) -> typing.Optional[bool] - output = self._backend.execute( + user_id: int, + group_name: str, + cache: bool = False, + cache_ttl: Optional[int] = None, + ) -> Optional[bool]: + output: Optional[bool] = self._backend.execute( 'has_membership_resolve', user_id=user_id, group_name=group_name, cache=cache, cache_ttl=cache_ttl, - ) # type: typing.Optional[bool] + ) return output def fetch_acl_options( self, - skip=0, # type: int - limit=10, # type: typing.Optional[int] - cache=False, # type: bool - cache_ttl=None, # type: typing.Optional[int] - ): - # type: (...) -> typing.Optional[typing.List[dict]] - output = self._backend.execute( + skip: int = 0, + limit: Optional[int] = 10, + cache: bool = False, + cache_ttl: Optional[int] = None, + ) -> Optional[List[Dict]]: + output: Optional[List[Dict]] = self._backend.execute( 'fetch_acl_options', skip=skip, limit=limit, cache=cache, cache_ttl=cache_ttl, - ) # type: typing.Optional[typing.List[dict]] + ) return output def get_unread_notifications_count( self, - user_id, # type: int - cache=False, # type: bool - cache_ttl=None, # type: typing.Optional[int] - ): - # type: (...) -> typing.Optional[dict] - output = self._backend.execute( + user_id: int, + cache: bool = False, + cache_ttl: Optional[int] = None, + ) -> Optional[Dict]: + output: Optional[Dict] = self._backend.execute( 'get_unread_notifications_count', user_id=user_id, cache=cache, cache_ttl=cache_ttl, - ) # type: typing.Optional[dict] + ) return output - def get_user_acl(self, raw_user_permissions): - # type: (str) -> flask_phpbb3.backends.base.UserAcl + def get_user_acl( + self, + raw_user_permissions: str + ) -> flask_phpbb3.backends.base.UserAcl: return self._backend.get_user_acl(raw_user_permissions) def execute_custom( self, - command, # type: str - cache=False, # type: bool - cache_ttl=None, # type: typing.Optional[int] - **kwargs # type: typing.Any - ): - # type: (...) -> typing.Any + command: str, + cache: bool = False, + cache_ttl: Optional[int] = None, + **kwargs: Any + ) -> Any: output = self._backend.execute( command, cache=cache, cache_ttl=cache_ttl, **kwargs - ) # type: typing.Any + ) return output - def teardown(self, exception): - # type: (typing.Any) -> None - ctx = flask._app_ctx_stack.top - if hasattr(ctx, 'phpbb3_backend'): - ctx.phpbb3_backend.close() + def teardown(self, exception: Any) -> None: + backend: flask_phpbb3.backends.base.BaseBackend =\ + flask.g.get('phpbb3_backend', None) + if backend is not None: + backend.close() From d47b4474c9ff4628ce5a07e891a1b0ffe03d9cd6 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 21:38:31 +0100 Subject: [PATCH 12/22] Update sessions.py, add error handling in extension.py --- flask_phpbb3/extension.py | 5 ++ flask_phpbb3/sessions.py | 103 ++++++++++++++++++++------------------ 2 files changed, 59 insertions(+), 49 deletions(-) diff --git a/flask_phpbb3/extension.py b/flask_phpbb3/extension.py index 03ca8c0..615f885 100644 --- a/flask_phpbb3/extension.py +++ b/flask_phpbb3/extension.py @@ -106,6 +106,11 @@ def _backend(self) -> flask_phpbb3.backends.base.BaseBackend: flask.g.get('phpbb3_backend', None) if backend is None or backend.is_closed: + if not hasattr(current_app, 'phpbb3_cache'): + raise ValueError( + 'App not properly configured, phpbb3_cache is missing!' + ) + cache = getattr(current_app, 'phpbb3_cache') backend = PhpBB3._create_backend( current_app.config['PHPBB3']['DRIVER'], diff --git a/flask_phpbb3/sessions.py b/flask_phpbb3/sessions.py index 436d25f..f9917eb 100644 --- a/flask_phpbb3/sessions.py +++ b/flask_phpbb3/sessions.py @@ -1,5 +1,7 @@ import json -import typing +from typing import Any, Dict, Optional, Set + +import cachelib import flask import flask.sessions @@ -8,66 +10,56 @@ import flask_phpbb3 import flask_phpbb3.backends.base -import cachelib -ANONYMOUS_CACHE_TTL = 3600 * 24 +ANONYMOUS_CACHE_TTL: int = 3600 * 24 -class PhpBB3Session(dict, flask.sessions.SessionMixin): - def __init__(self): - # type: () -> None +class PhpBB3Session(Dict[str, Any], flask.sessions.SessionMixin): + def __init__(self) -> None: # Some session related variables - self.modified = False - self.new = False - self._read_only_properties = set([]) # type: set + self.modified: bool = False + self.new: bool = False + self._read_only_properties: Set[str] = set([]) # Some ACL related things - self._acl = None\ - # type: typing.Optional[flask_phpbb3.backends.base.UserAcl] + self._acl: Optional[flask_phpbb3.backends.base.UserAcl] = None # Per request cache # This should not be cached into session, but per # request should not be executed multiple times - self._request_cache = {} # type: dict + self._request_cache: Dict[str, Any] = {} - def __setitem__(self, key, value): - # type: (str, typing.Union[str, int]) -> None - modified = self.get(key) != value + def __setitem__(self, key: str, value: str | int) -> None: + modified: bool = self.get(key) != value super(PhpBB3Session, self).__setitem__(key, value) if key not in self._read_only_properties: self.modified |= modified - def __delitem__(self, key): - # type: (str) -> None + def __delitem__(self, key: str) -> None: super(PhpBB3Session, self).__delitem__(key) self.modified = True @property - def _phpbb3(self): - # type: () -> flask_phpbb3.PhpBB3 - output = flask.current_app.phpbb3 # type: flask_phpbb3.PhpBB3 + def _phpbb3(self) -> flask_phpbb3.PhpBB3: + output: flask_phpbb3.PhpBB3 = getattr(flask.current_app, 'phpbb3') return output - def pop(self, *args, **kwargs): - # type: (*typing.Any, **typing.Any) -> typing.Any + def pop(self, *args: Any, **kwargs: Any) -> Any: """Wrapper to set modified.""" self.modified = True return super(PhpBB3Session, self).pop(*args, **kwargs) - def clear(self): - # type: () -> None + def clear(self) -> None: self.modified = True return super(PhpBB3Session, self).clear() @property - def is_authenticated(self): - # type: () -> bool + def is_authenticated(self) -> bool: """Helper method to test if user is authenticated.""" user_id = int(self.get('user_id', 1)) return user_id > 1 - def is_member(self, group): - # type: (typing.Union[int, str]) -> bool + def is_member(self, group: int | str) -> bool: """Tests if user is a member of specified group.""" if isinstance(group, int): # Try with default group @@ -90,24 +82,21 @@ def is_member(self, group): return False return output - def has_privilege(self, option, forum_id=0): - # type: (str, int) -> bool + def has_privilege(self, option: str, forum_id: int = 0) -> bool: """Test if user has global or local (if forum_id is set) privileges.""" if not self._acl: self._acl = self._phpbb3.get_user_acl(self['user_permissions']) return self._acl.has_privilege(option, forum_id) - def has_privileges(self, *options, **kwargs): - # type: (*str, **int) -> bool + def has_privileges(self, *options: str, **kwargs: int) -> bool: forum_id = kwargs.get('forum_id', 0) if not self._acl: self._acl = self._phpbb3.get_user_acl(self['user_permissions']) return self._acl.has_privileges(*options, forum_id=forum_id) - def get_link_hash(self, link): - # type: (str) -> str + def get_link_hash(self, link: str) -> str: """Returns link hash.""" if not self.is_authenticated: return '' @@ -116,8 +105,7 @@ def get_link_hash(self, link): return hashlib.sha1(self['user_form_salt'] + link).hexdigest()[:8] @property - def num_unread_notifications(self): - # type: () -> int + def num_unread_notifications(self) -> int: """Returns number of unread notifications.""" if 'num_unread_notifications' not in self._request_cache: result = self._phpbb3.get_unread_notifications_count( @@ -140,31 +128,41 @@ class PhpBB3SessionInterface(flask.sessions.SessionInterface): session_class = PhpBB3Session @classmethod - def _cache(cls, app): - # type: (flask.Flask) -> werkzeug.contrib.cache.BaseCache - output = app.phpbb3_cache # type: werkzeug.contrib.cache.BaseCache + def _cache(cls, app: flask.Flask) -> cachelib.BaseCache: + if not hasattr(app, 'phpbb3_cache'): + raise ValueError( + 'App not properly configured, phpbb3_cache is missing!' + ) + output: cachelib.BaseCache = getattr(app, 'phpbb3_cache') return output - def _is_bot(self, app, request): + def _is_bot( + self, + app: flask.Flask, + request: flask.wrappers.Request + ) -> bool: for user_agent in app.config['PHPBB3_BOTLIST']: if request.headers.get('User-Agent', '').startswith(user_agent): return True return False - def open_session(self, app, request): - # type: (flask.Flask, flask.wrappers.Request) -> PhpBB3Session - cookie_name = app.config.get('PHPBB3_COOKIE_NAME', 'phpbb3_') + def open_session( + self, + app: flask.Flask, + request: flask.wrappers.Request + ) -> PhpBB3Session: + cookie_name: str = app.config.get('PHPBB3_COOKIE_NAME', 'phpbb3_') if not hasattr(app, 'phpbb3'): raise ValueError('App not properly configured, phpbb3 is missing!') - phpbb3 = app.phpbb3 # type: flask_phpbb3.PhpBB3 + phpbb3: flask_phpbb3.PhpBB3 = getattr(app, 'phpbb3') - session_id = request.args.get('sid', type=str)\ + session_id: Optional[str] = request.args.get('sid', type=str)\ or request.cookies.get(cookie_name + 'sid', None) if not session_id: session_id = None - user = None + user: Optional[Dict] = None if self._is_bot(app, request): user = {'user_id': 1, 'username': 'Anonymous'} elif session_id: @@ -181,7 +179,7 @@ def open_session(self, app, request): ) # Create session - session = self.session_class() + session: PhpBB3Session = self.session_class() # Set session data if isinstance(user, dict) and user: @@ -204,8 +202,15 @@ def open_session(self, app, request): return session - def save_session(self, app, session, response): - # type: (flask.Flask, PhpBB3Session, flask.wrappers.Response) -> None + def save_session( + self, + app: flask.Flask, + session: flask.sessions.SessionMixin, + response: flask.wrappers.Response + ) -> None: + if not isinstance(session, PhpBB3Session): + raise TypeError('The provided session is not PhpBB3Session!') + """Currenlty does nothing.""" if session.modified and session._read_only_properties: # Store all 'storable' properties From 2c571ede484ebd642a6131fcdd07bcef03a396c8 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 21:45:51 +0100 Subject: [PATCH 13/22] Add comments --- flask_phpbb3/sessions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask_phpbb3/sessions.py b/flask_phpbb3/sessions.py index f9917eb..ccb6448 100644 --- a/flask_phpbb3/sessions.py +++ b/flask_phpbb3/sessions.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, Optional, Set +from typing import Any, Dict, Mapping, MutableMapping, Optional, Set import cachelib @@ -14,6 +14,7 @@ ANONYMOUS_CACHE_TTL: int = 3600 * 24 +# TODO: Inheriting Dict causes a type-checker error, possibly restructure class PhpBB3Session(Dict[str, Any], flask.sessions.SessionMixin): def __init__(self) -> None: # Some session related variables From 39a6f904af2ded025b8746d5d08fa074721513f0 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Fri, 26 Dec 2025 22:18:06 +0100 Subject: [PATCH 14/22] Fixing tests WIP --- docker-compose.yml | 2 ++ flask_phpbb3/__init__.py | 4 ++-- tests/integration/base.py | 3 --- tests/integration/test_extension.py | 4 +--- tests/unit/backends/test_base.py | 4 +--- tests/unit/backends/test_psycopg2.py | 18 ++++++++---------- tests/unit/test_sessions.py | 4 +--- 7 files changed, 15 insertions(+), 24 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 48d09a3..97bbdb4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,3 +5,5 @@ services: image: postgres:9.6 ports: - 5432:5432 + environment: + POSTGRES_HOST_AUTH_METHOD: trust \ No newline at end of file diff --git a/flask_phpbb3/__init__.py b/flask_phpbb3/__init__.py index 86d99e0..9035f1a 100644 --- a/flask_phpbb3/__init__.py +++ b/flask_phpbb3/__init__.py @@ -1,6 +1,6 @@ -import pkg_resources +import importlib.metadata -__version__ = pkg_resources.get_distribution(__name__).version +__version__ = importlib.metadata.distribution(__name__).version from .extension import PhpBB3 diff --git a/tests/integration/base.py b/tests/integration/base.py index 5d394c4..472ba0f 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import typing import unittest @@ -9,7 +7,6 @@ import psycopg2 import psycopg2.extensions -import psycopg2.extras DB_HOST = '127.0.0.1' diff --git a/tests/integration/test_extension.py b/tests/integration/test_extension.py index 177145a..90417d3 100644 --- a/tests/integration/test_extension.py +++ b/tests/integration/test_extension.py @@ -1,6 +1,4 @@ -from __future__ import absolute_import - -import mock +from unittest import mock from . import base diff --git a/tests/unit/backends/test_base.py b/tests/unit/backends/test_base.py index 8559c8f..0d06ad6 100644 --- a/tests/unit/backends/test_base.py +++ b/tests/unit/backends/test_base.py @@ -1,10 +1,8 @@ -from __future__ import absolute_import - import unittest import flask_phpbb3.backends.base -import mock +from unittest import mock class TestSessionHasPrivilege(unittest.TestCase): diff --git a/tests/unit/backends/test_psycopg2.py b/tests/unit/backends/test_psycopg2.py index d8a16cd..f36f55f 100644 --- a/tests/unit/backends/test_psycopg2.py +++ b/tests/unit/backends/test_psycopg2.py @@ -1,12 +1,10 @@ -from __future__ import absolute_import - import unittest import flask_phpbb3.backends.psycopg2 -import mock +from unittest import mock -import werkzeug.contrib.cache +import cachelib @mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend._db') @@ -14,7 +12,7 @@ class TestExecuteOperation(unittest.TestCase): def setUp(self): # type: () -> None self.connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( - werkzeug.contrib.cache.SimpleCache(), + cachelib.SimpleCache(), { 'TABLE_PREFIX': '', } @@ -112,7 +110,7 @@ def test_unknown_query(self, mocked_db): class TestPreparedCustomFieldsStatements(unittest.TestCase): def test_empty(self, mocked_db): connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( - werkzeug.contrib.cache.SimpleCache(), + cachelib.SimpleCache(), { 'TABLE_PREFIX': '', 'CUSTOM_USER_FIELDS': [], @@ -135,7 +133,7 @@ def test_empty(self, mocked_db): def test_valid(self, mocked_db): connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( - werkzeug.contrib.cache.SimpleCache(), + cachelib.SimpleCache(), { 'TABLE_PREFIX': '', 'CUSTOM_USER_FIELDS': ['some_field', 'another_field'], @@ -163,7 +161,7 @@ def test_valid(self, mocked_db): class TestPreparedCustomStatements(unittest.TestCase): def test_empty(self, mocked_db): connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( - werkzeug.contrib.cache.SimpleCache(), + cachelib.SimpleCache(), { 'TABLE_PREFIX': '', 'CUSTOM_STATEMENTS': {}, @@ -186,7 +184,7 @@ def test_empty(self, mocked_db): def test_addition(self, mocked_db): connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( - werkzeug.contrib.cache.SimpleCache(), + cachelib.SimpleCache(), { 'TABLE_PREFIX': '', 'CUSTOM_STATEMENTS': { @@ -216,7 +214,7 @@ def test_addition(self, mocked_db): def test_override(self, mocked_db): connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( - werkzeug.contrib.cache.SimpleCache(), + cachelib.SimpleCache(), { 'TABLE_PREFIX': '', 'CUSTOM_STATEMENTS': { diff --git a/tests/unit/test_sessions.py b/tests/unit/test_sessions.py index a5c12b4..e3a583d 100644 --- a/tests/unit/test_sessions.py +++ b/tests/unit/test_sessions.py @@ -1,11 +1,9 @@ -from __future__ import absolute_import - import hashlib import unittest import flask_phpbb3.sessions -import mock +from unittest import mock class TestSession(unittest.TestCase): From 69802842ec33a1e2f2e70f10a8d3a010dbb94f1a Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Sat, 27 Dec 2025 13:15:48 +0100 Subject: [PATCH 15/22] Update requirements, adjust tests for this Flask version --- flask_phpbb3/sessions.py | 7 +++--- requirements.txt | 6 +++--- requirements/dev-3.txt | 2 +- tests/integration/base.py | 6 ++++-- tests/integration/test_extension.py | 30 ++++++++++++++------------ tests/unit/backends/test_psycopg2.py | 32 ++++++++++++++-------------- tests/unit/test_sessions.py | 4 ++-- 7 files changed, 46 insertions(+), 41 deletions(-) diff --git a/flask_phpbb3/sessions.py b/flask_phpbb3/sessions.py index ccb6448..aade079 100644 --- a/flask_phpbb3/sessions.py +++ b/flask_phpbb3/sessions.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, Mapping, MutableMapping, Optional, Set +from typing import Any, Dict, Optional, Set import cachelib @@ -103,7 +103,8 @@ def get_link_hash(self, link: str) -> str: return '' import hashlib - return hashlib.sha1(self['user_form_salt'] + link).hexdigest()[:8] + prepared_link: str = self['user_form_salt'] + link + return hashlib.sha1(prepared_link.encode('utf-8')).hexdigest()[:8] @property def num_unread_notifications(self) -> int: @@ -170,7 +171,7 @@ def open_session( # Try to fetch session user = phpbb3.get_session(session_id=session_id) if user and 'username' in user: - user['username'] = user['username'].decode('utf-8', 'ignore') + user['username'] = user['username'] if not user: # Use anonymous user user = phpbb3.get_user( diff --git a/requirements.txt b/requirements.txt index 6360346..d337766 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -psycopg2>=2.9.11 -Flask>=3.1.2 -cachelib>=0.13.0 +psycopg2>=2.4.5 +Flask==3.1.2 +cachelib==0.13.0 setuptools>=80.9.0 \ No newline at end of file diff --git a/requirements/dev-3.txt b/requirements/dev-3.txt index a030f67..111e389 100644 --- a/requirements/dev-3.txt +++ b/requirements/dev-3.txt @@ -1,2 +1,2 @@ -r dev.txt -mypy==0.660 +mypy>=1.19.1 diff --git a/tests/integration/base.py b/tests/integration/base.py index 472ba0f..cae869f 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -15,7 +15,7 @@ DB_NAME = 'phpbb3_test' -def setUpModule(): +def setUpModule() -> None: # type: () -> None _create_db() connection = _get_connection(DB_HOST, DB_USER, DB_NAME) @@ -44,6 +44,7 @@ def setUp(self): 'PASSWORD': '', 'TABLE_PREFIX': 'phpbb_', }, + 'SERVER_NAME': '127.0.0.1' }) self.phpbb3 = flask_phpbb3.PhpBB3(self.app) @@ -164,7 +165,8 @@ def _create_db(): def _init_schema(connection): # type: (psycopg2.extensions.connection) -> None - schema_sql = open('./tests/fixtures/postgres/schema.sql', 'r').read() + with open('./tests/fixtures/postgres/schema.sql', 'r') as f: + schema_sql = f.read() cursor_schema = connection.cursor() # type: psycopg2.extensions.cursor cursor_schema.execute(schema_sql) cursor_schema.close() diff --git a/tests/integration/test_extension.py b/tests/integration/test_extension.py index 90417d3..4850803 100644 --- a/tests/integration/test_extension.py +++ b/tests/integration/test_extension.py @@ -76,14 +76,14 @@ def setUp(self): def test_anonymous(self): # type: () -> None - data = self.client.get('/').data + data = self.client.get('/').get_data().decode('utf-8') self.assertEqual(data, '1,Anonymous') def test_invalid_session(self): # type: () -> None base._create_user(self.cursor) - data = self.client.get('/?sid=123').data + data = self.client.get('/?sid=123').get_data().decode('utf-8') self.assertEqual(data, '1,Anonymous') def test_user_by_args(self): @@ -91,7 +91,9 @@ def test_user_by_args(self): base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - data = self.client.get('/?sid=' + self.session_id).data + data = self.client.get('/?sid=' + self.session_id)\ + .get_data()\ + .decode('utf-8') self.assertEqual(data, '2,test') def test_user_by_cookie(self): @@ -99,33 +101,33 @@ def test_user_by_cookie(self): base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - self.client.set_cookie('127.0.0.1', 'phpbb3_sid', self.session_id) - data = self.client.get('/').data + self.client.set_cookie('phpbb3_sid', self.session_id, domain='127.0.0.1') + data = self.client.get('/').get_data().decode('utf-8') self.assertEqual(data, '2,test') - self.client.delete_cookie('127.0.0.1', 'phpbb3_sid') + self.client.delete_cookie('phpbb3_sid', domain='127.0.0.1') def test_storage(self): # type: () -> None base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - self.client.set_cookie('127.0.0.1', 'phpbb3_sid', self.session_id) - data = self.client.get('/data').data + self.client.set_cookie('phpbb3_sid', self.session_id, domain='127.0.0.1') + data = self.client.get('/data').get_data().decode('utf-8') self.assertEqual(data, '') self.client.get('/data/something') - data = self.client.get('/data').data + data = self.client.get('/data').get_data().decode('utf-8') self.assertEqual(data, 'something') def test_storage_invalid_id(self): # type: () -> None - data = self.client.get('/data').data + data = self.client.get('/data').get_data().decode('utf-8') self.assertEqual(data, '') self.client.get('/data/something') - data = self.client.get('/data').data + data = self.client.get('/data').get_data().decode('utf-8') self.assertEqual(data, '') def test_privilege(self): @@ -135,11 +137,11 @@ def test_privilege(self): base._create_privilege(self.cursor, 1, 'm_edit') base._grant_privilege(self.cursor, 2) - data = self.client.get('/priv_test').data + data = self.client.get('/priv_test').get_data().decode('utf-8') self.assertEqual(data, 'False,False,False') # We do a login via phpbb3 :P - self.client.set_cookie('127.0.0.1', 'phpbb3_sid', self.session_id) + self.client.set_cookie('phpbb3_sid', self.session_id, domain='127.0.0.1') - data = self.client.get('/priv_test').data + data = self.client.get('/priv_test').get_data().decode('utf-8') self.assertEqual(data, 'True,False,True') diff --git a/tests/unit/backends/test_psycopg2.py b/tests/unit/backends/test_psycopg2.py index f36f55f..0a98159 100644 --- a/tests/unit/backends/test_psycopg2.py +++ b/tests/unit/backends/test_psycopg2.py @@ -117,9 +117,9 @@ def test_empty(self, mocked_db): } ) - self.assertListEqual( - connection._functions.keys(), - [ + self.assertSetEqual( + set(connection._functions.keys()), + set([ 'has_membership_resolve', 'get_autologin', 'get_session', @@ -128,7 +128,7 @@ def test_empty(self, mocked_db): 'get_unread_notifications_count', 'get_user', 'get_user_profile', - ] + ]) ) def test_valid(self, mocked_db): @@ -168,9 +168,9 @@ def test_empty(self, mocked_db): } ) - self.assertListEqual( - connection._functions.keys(), - [ + self.assertSetEqual( + set(connection._functions.keys()), + set([ 'has_membership_resolve', 'get_autologin', 'get_session', @@ -179,7 +179,7 @@ def test_empty(self, mocked_db): 'get_unread_notifications_count', 'get_user', 'get_user_profile', - ] + ]) ) def test_addition(self, mocked_db): @@ -193,9 +193,9 @@ def test_addition(self, mocked_db): } ) - self.assertListEqual( - connection._functions.keys(), - [ + self.assertSetEqual( + set(connection._functions.keys()), + set([ 'has_membership_resolve', 'get_autologin', 'get_session', @@ -205,7 +205,7 @@ def test_addition(self, mocked_db): 'some_custom_statement', 'get_user', 'get_user_profile', - ] + ]) ) self.assertEqual( connection._functions['some_custom_statement'], @@ -223,9 +223,9 @@ def test_override(self, mocked_db): } ) - self.assertListEqual( - connection._functions.keys(), - [ + self.assertSetEqual( + set(connection._functions.keys()), + set([ 'has_membership_resolve', 'get_autologin', 'get_session', @@ -234,7 +234,7 @@ def test_override(self, mocked_db): 'get_unread_notifications_count', 'get_user', 'get_user_profile', - ] + ]) ) self.assertEqual( connection._functions['get_autologin'], diff --git a/tests/unit/test_sessions.py b/tests/unit/test_sessions.py index e3a583d..bdeb57e 100644 --- a/tests/unit/test_sessions.py +++ b/tests/unit/test_sessions.py @@ -96,8 +96,8 @@ def test_get_link_hash(self): self.assertEqual(self.session.get_link_hash(some_link), '') self.session['user_id'] = '3' - salted_link = self.session['user_form_salt'] + some_link - expected_value = hashlib.sha1(salted_link).hexdigest()[:8] + salted_link: str = self.session['user_form_salt'] + some_link + expected_value = hashlib.sha1(salted_link.encode('utf-8')).hexdigest()[:8] self.assertEqual(self.session.get_link_hash(some_link), expected_value) From a1c214ebbbb099a1dda5f0adbbeb85df1abcdbce Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Sat, 27 Dec 2025 13:50:58 +0100 Subject: [PATCH 16/22] Format tests --- tests/integration/base.py | 74 ++++++++++++----------- tests/integration/test_extension.py | 87 +++++++++++++++------------ tests/unit/backends/test_base.py | 33 ++++------- tests/unit/backends/test_psycopg2.py | 36 +++++------- tests/unit/test_sessions.py | 88 ++++++++++------------------ 5 files changed, 141 insertions(+), 177 deletions(-) diff --git a/tests/integration/base.py b/tests/integration/base.py index cae869f..bb6a62d 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -1,4 +1,3 @@ -import typing import unittest import flask @@ -16,21 +15,18 @@ def setUpModule() -> None: - # type: () -> None _create_db() connection = _get_connection(DB_HOST, DB_USER, DB_NAME) _init_schema(connection) connection.commit() -def tearDownModule(): - # type: () -> None +def tearDownModule() -> None: _destory_db() class TestWithDatabase(unittest.TestCase): - def setUp(self): - # type: () -> None + def setUp(self) -> None: self.app = flask.Flask('test_app') self.app.config.update({ 'PHPBB3': { @@ -50,30 +46,26 @@ def setUp(self): # From these lines devil is born @self.app.route('/') - def index(): - # type: () -> typing.Any + def index() -> str: return flask.render_template_string( '{{ session.user_id}},{{ session.username }}' ) @self.app.route('/data') - def data(): - # type: () -> typing.Any + def data() -> str: return flask.render_template_string( '{{ session.custom_var}}' ) @self.app.route('/data/') - def set_data(package): - # type: (str) -> typing.Any + def set_data(package: str) -> str: flask.session['custom_var'] = package return flask.render_template_string( 'Done :o' ) @self.app.route('/priv_test') - def test_privileges(): - # type: () -> typing.Any + def test_privileges() -> str: return flask.render_template_string( "{{ session.has_privilege('m_edit') }}," "{{ session.has_privilege('m_delete') }}," @@ -84,23 +76,21 @@ def test_privileges(): self.ctx.push() # Init connection - self.connection = self.phpbb3._backend._db - self.cursor = self.connection.cursor()\ - # type: psycopg2.extensions.cursor + self.connection: psycopg2.extensions.connection =\ + self.phpbb3._backend._db + self.cursor: psycopg2.extensions.cursor = self.connection.cursor() # Setup client self.client = self.app.test_client() - def tearDown(self): - # type: () -> None + def tearDown(self) -> None: self.connection.rollback() self.cursor.close() self.ctx.pop() -def _create_user(cursor): - # type: (psycopg2.extensions.cursor) -> None +def _create_user(cursor: psycopg2.extensions.cursor) -> None: cursor.execute( "insert into" " phpbb_users (user_id, username, username_clean)" @@ -108,8 +98,11 @@ def _create_user(cursor): ) -def _create_session(cursor, session_id, user_id): - # type: (psycopg2.extensions.cursor, str, int) -> None +def _create_session( + cursor: psycopg2.extensions.cursor, + session_id: str, + user_id: int +) -> None: cursor.execute( "insert into" " phpbb_sessions (session_id, session_user_id)" @@ -120,8 +113,11 @@ def _create_session(cursor, session_id, user_id): ) -def _create_privilege(cursor, privilege_id, privilege): - # type: (psycopg2.extensions.cursor, int, str) -> None +def _create_privilege( + cursor: psycopg2.extensions.cursor, + privilege_id: int, + privilege: str +) -> None: cursor.execute( "insert into" " phpbb_acl_options (auth_option_id, auth_option, is_global)" @@ -132,8 +128,10 @@ def _create_privilege(cursor, privilege_id, privilege): ) -def _grant_privilege(cursor, user_id): - # type: (psycopg2.extensions.cursor, int) -> None +def _grant_privilege( + cursor: psycopg2.extensions.cursor, + user_id: int +) -> None: # Cryptic value to allow only m_edit permission permission_set = 'HRA0HS' cursor.execute( @@ -147,12 +145,11 @@ def _grant_privilege(cursor, user_id): ) -def _create_db(): - # type: () -> None +def _create_db() -> None: connection = _get_connection(DB_HOST, DB_ROOT_USER, DB_ROOT_USER) connection.set_isolation_level(0) - cursor = connection.cursor() # type: psycopg2.extensions.cursor + cursor = connection.cursor() cursor.execute('create user {user}'.format(user=DB_USER)) cursor.execute('create database {db_name} owner {user};'.format( user=DB_USER, @@ -163,21 +160,19 @@ def _create_db(): connection.close() -def _init_schema(connection): - # type: (psycopg2.extensions.connection) -> None +def _init_schema(connection: psycopg2.extensions.connection) -> None: with open('./tests/fixtures/postgres/schema.sql', 'r') as f: schema_sql = f.read() - cursor_schema = connection.cursor() # type: psycopg2.extensions.cursor + cursor_schema = connection.cursor() cursor_schema.execute(schema_sql) cursor_schema.close() -def _destory_db(): - # type: () -> None +def _destory_db() -> None: connection = _get_connection(DB_HOST, DB_ROOT_USER, DB_ROOT_USER) connection.set_isolation_level(0) - cursor = connection.cursor() # type: psycopg2.extensions.cursor + cursor = connection.cursor() cursor.execute('drop database {db_name};'.format( db_name=DB_NAME, ) @@ -187,8 +182,11 @@ def _destory_db(): connection.close() -def _get_connection(host, user, database): - # type: (str, str, str) -> psycopg2.extensions.connection +def _get_connection( + host: str, + user: str, + database: str +) -> psycopg2.extensions.connection: connection_string = ( 'dbname={db_name}' ' user={user}' diff --git a/tests/integration/test_extension.py b/tests/integration/test_extension.py index 4850803..785daa7 100644 --- a/tests/integration/test_extension.py +++ b/tests/integration/test_extension.py @@ -1,5 +1,7 @@ from unittest import mock +from flask_phpbb3 import PhpBB3 + from . import base setUpModule = base.setUpModule @@ -8,37 +10,41 @@ class TestExtension(base.TestWithDatabase): @mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend.close') - def test_teardown(self, mocked_close): - # type: (mock.Mock) -> None + def test_teardown(self, mocked_close: mock.Mock) -> None: self.ctx.pop() mocked_close.assert_called() self.ctx.push() class TestGetUser(base.TestWithDatabase): - def test_anonymous_user(self): - # type: () -> None - anonymous_user = self.app.phpbb3.get_user(user_id=1) - self.assertEqual(anonymous_user['username'], 'Anonymous') - self.assertEqual(anonymous_user['user_id'], 1) - - def test_user(self): - # type: () -> None + def test_anonymous_user(self) -> None: + phpbb3: PhpBB3 = getattr(self.app, 'phpbb3') + anonymous_user = phpbb3.get_user(user_id=1) + + self.assertIsNotNone(anonymous_user) + if anonymous_user is not None: + self.assertEqual(anonymous_user['username'], 'Anonymous') + self.assertEqual(anonymous_user['user_id'], 1) + + def test_user(self) -> None: base._create_user(self.cursor) - user = self.app.phpbb3.get_user(user_id=2) - self.assertEqual(user['username'], 'test') - self.assertEqual(user['user_id'], 2) + phpbb3: PhpBB3 = getattr(self.app, 'phpbb3') + user = phpbb3.get_user(user_id=2) + + self.assertIsNotNone(user) + if user is not None: + self.assertEqual(user['username'], 'test') + self.assertEqual(user['user_id'], 2) - def test_unknown_user(self): - # type: () -> None - unknown_user = self.app.phpbb3.get_user(user_id=2) + def test_unknown_user(self) -> None: + phpbb3: PhpBB3 = getattr(self.app, 'phpbb3') + unknown_user = phpbb3.get_user(user_id=2) self.assertEqual(unknown_user, None) class TestFetch(base.TestWithDatabase): - def test_paging(self): - # type: () -> None + def test_paging(self) -> None: base._create_privilege(self.cursor, 1, 'm_edit') base._create_privilege(self.cursor, 2, 'm_delete') base._create_privilege(self.cursor, 3, 'm_some_random') @@ -64,30 +70,27 @@ def test_paging(self): }]), (3, [])] for skip in range(0, 4): - privilege = self.app.phpbb3.fetch_acl_options(skip=skip, limit=1) + phpbb3: PhpBB3 = getattr(self.app, 'phpbb3') + privilege = phpbb3.fetch_acl_options(skip=skip, limit=1) self.assertEqual((skip, privilege), expected_privileges[skip]) class TestSession(base.TestWithDatabase): - def setUp(self): - # type: () -> None + def setUp(self) -> None: super(TestSession, self).setUp() self.session_id = '123' - def test_anonymous(self): - # type: () -> None + def test_anonymous(self) -> None: data = self.client.get('/').get_data().decode('utf-8') self.assertEqual(data, '1,Anonymous') - def test_invalid_session(self): - # type: () -> None + def test_invalid_session(self) -> None: base._create_user(self.cursor) data = self.client.get('/?sid=123').get_data().decode('utf-8') self.assertEqual(data, '1,Anonymous') - def test_user_by_args(self): - # type: () -> None + def test_user_by_args(self) -> None: base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) @@ -96,22 +99,28 @@ def test_user_by_args(self): .decode('utf-8') self.assertEqual(data, '2,test') - def test_user_by_cookie(self): - # type: () -> None + def test_user_by_cookie(self) -> None: base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - self.client.set_cookie('phpbb3_sid', self.session_id, domain='127.0.0.1') + self.client.set_cookie( + 'phpbb3_sid', + self.session_id, + domain='127.0.0.1' + ) data = self.client.get('/').get_data().decode('utf-8') self.assertEqual(data, '2,test') self.client.delete_cookie('phpbb3_sid', domain='127.0.0.1') - def test_storage(self): - # type: () -> None + def test_storage(self) -> None: base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - self.client.set_cookie('phpbb3_sid', self.session_id, domain='127.0.0.1') + self.client.set_cookie( + 'phpbb3_sid', + self.session_id, + domain='127.0.0.1' + ) data = self.client.get('/data').get_data().decode('utf-8') self.assertEqual(data, '') @@ -120,8 +129,7 @@ def test_storage(self): data = self.client.get('/data').get_data().decode('utf-8') self.assertEqual(data, 'something') - def test_storage_invalid_id(self): - # type: () -> None + def test_storage_invalid_id(self) -> None: data = self.client.get('/data').get_data().decode('utf-8') self.assertEqual(data, '') @@ -130,8 +138,7 @@ def test_storage_invalid_id(self): data = self.client.get('/data').get_data().decode('utf-8') self.assertEqual(data, '') - def test_privilege(self): - # type: () -> None + def test_privilege(self) -> None: base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) base._create_privilege(self.cursor, 1, 'm_edit') @@ -141,7 +148,11 @@ def test_privilege(self): self.assertEqual(data, 'False,False,False') # We do a login via phpbb3 :P - self.client.set_cookie('phpbb3_sid', self.session_id, domain='127.0.0.1') + self.client.set_cookie( + 'phpbb3_sid', + self.session_id, + domain='127.0.0.1' + ) data = self.client.get('/priv_test').get_data().decode('utf-8') self.assertEqual(data, 'True,False,True') diff --git a/tests/unit/backends/test_base.py b/tests/unit/backends/test_base.py index 0d06ad6..633f8c0 100644 --- a/tests/unit/backends/test_base.py +++ b/tests/unit/backends/test_base.py @@ -1,13 +1,11 @@ import unittest +from unittest import mock import flask_phpbb3.backends.base -from unittest import mock - class TestSessionHasPrivilege(unittest.TestCase): - def setUp(self): - # type: () -> None + def setUp(self) -> None: user_acl = { '0': ['0'] * 31, '5': ['0'] * 31, @@ -37,8 +35,7 @@ def setUp(self): '5': ''.join(user_acl['5']), } - def test_existing(self): - # type: () -> None + def test_existing(self) -> None: actual_result = self.user_acl.has_privilege( 'm_edit', ) @@ -49,8 +46,7 @@ def test_existing(self): ) self.assertFalse(actual_result) - def test_global(self): - # type: () -> None + def test_global(self) -> None: # m_review is not global, and is false (even tho on global the index # is set to true) actual_result = self.user_acl.has_privilege( @@ -65,8 +61,7 @@ def test_global(self): ) self.assertTrue(actual_result) - def test_local(self): - # type: () -> None + def test_local(self) -> None: # Now, this is True since we are on local level actual_result = self.user_acl.has_privilege( 'm_review', @@ -93,8 +88,7 @@ def test_local(self): ) self.assertFalse(actual_result) - def test_negated(self): - # type: () -> None + def test_negated(self) -> None: actual_result = self.user_acl.has_privilege( '!m_review', forum_id=5 @@ -117,8 +111,7 @@ def test_negated(self): ) self.assertFalse(actual_result) - def test_out_of_bound(self): - # type: () -> None + def test_out_of_bound(self) -> None: actual_result = self.user_acl.has_privilege( 'm_strange' ) @@ -130,8 +123,7 @@ def test_out_of_bound(self): ) self.assertFalse(actual_result) - def test_unknown(self): - # type: () -> None + def test_unknown(self) -> None: actual_result = self.user_acl.has_privilege( 'm_unknown' ) @@ -151,16 +143,14 @@ def test_unknown(self): class TestSessionHasPrivileges(unittest.TestCase): - def setUp(self): - # type: () -> None + def setUp(self) -> None: self.user_acl = flask_phpbb3.backends.base.UserAcl( raw_acl_options=[], raw_user_permissions='', ) @mock.patch('flask_phpbb3.backends.base.UserAcl.has_privilege') - def test_combinations(self, has_privilege_mock): - # type: (mock.Mock) -> None + def test_combinations(self, has_privilege_mock: mock.Mock) -> None: privileges = ('m_edit', 'm_delete', 'm_view') has_privilege_mock.return_value = False @@ -181,8 +171,7 @@ def test_combinations(self, has_privilege_mock): @mock.patch('flask_phpbb3.backends.base.UserAcl.has_privilege', return_value=False) - def test_per_forum(self, has_privilege_mock): - # type: (mock.Mock) -> None + def test_per_forum(self, has_privilege_mock: mock.Mock) -> None: privileges = ('m_edit', 'm_delete', 'm_view') self.user_acl.has_privileges(*privileges) diff --git a/tests/unit/backends/test_psycopg2.py b/tests/unit/backends/test_psycopg2.py index 0a98159..4e2b382 100644 --- a/tests/unit/backends/test_psycopg2.py +++ b/tests/unit/backends/test_psycopg2.py @@ -1,16 +1,14 @@ import unittest - -import flask_phpbb3.backends.psycopg2 - from unittest import mock import cachelib +import flask_phpbb3.backends.psycopg2 + @mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend._db') class TestExecuteOperation(unittest.TestCase): - def setUp(self): - # type: () -> None + def setUp(self) -> None: self.connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { @@ -18,8 +16,7 @@ def setUp(self): } ) - def test_get_query(self, mocked_db): - # type: (mock.Mock) -> None + def test_get_query(self, mocked_db: mock.Mock) -> None: parameters = mock.Mock() cursor = mock.Mock() cursor.fetchone.return_value = {'key': 'value'} @@ -37,8 +34,7 @@ def test_get_query(self, mocked_db): parameters ) - def test_has_query_negative(self, mocked_db): - # type: (mock.Mock) -> None + def test_has_query_negative(self, mocked_db: mock.Mock) -> None: cursor = mock.Mock() cursor.fetchone.return_value = 0 mocked_db.cursor.return_value = cursor @@ -51,8 +47,7 @@ def test_has_query_negative(self, mocked_db): self.assertFalse(actual_value) - def test_has_query_positive(self, mocked_db): - # type: (mock.Mock) -> None + def test_has_query_positive(self, mocked_db: mock.Mock) -> None: cursor = mock.Mock() cursor.fetchone.return_value = 1 mocked_db.cursor.return_value = cursor @@ -65,8 +60,7 @@ def test_has_query_positive(self, mocked_db): self.assertTrue(actual_value) - def test_fetch_query(self, mocked_db): - # type: (mock.Mock) -> None + def test_fetch_query(self, mocked_db: mock.Mock) -> None: expected_value = [{'key': 1}, {'key': 2}] cursor = mock.Mock() cursor.__iter__ = mock.Mock(return_value=iter(expected_value)) @@ -80,8 +74,7 @@ def test_fetch_query(self, mocked_db): self.assertEqual(actual_value, expected_value) - def test_set_query(self, mocked_db): - # type: (mock.Mock) -> None + def test_set_query(self, mocked_db: mock.Mock) -> None: cursor = mock.Mock() mocked_db.cursor.return_value = cursor @@ -94,8 +87,7 @@ def test_set_query(self, mocked_db): self.assertEqual(actual_value, cursor.statusmessage) mocked_db.commit.assert_called_once_with() - def test_unknown_query(self, mocked_db): - # type: (mock.Mock) -> None + def test_unknown_query(self, mocked_db: mock.Mock) -> None: actual_value = self.connection._execute_operation( 'unknown_op', 'select * from somewhere', @@ -108,7 +100,7 @@ def test_unknown_query(self, mocked_db): @mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend._db') class TestPreparedCustomFieldsStatements(unittest.TestCase): - def test_empty(self, mocked_db): + def test_empty(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { @@ -131,7 +123,7 @@ def test_empty(self, mocked_db): ]) ) - def test_valid(self, mocked_db): + def test_valid(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { @@ -159,7 +151,7 @@ def test_valid(self, mocked_db): @mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend._db') class TestPreparedCustomStatements(unittest.TestCase): - def test_empty(self, mocked_db): + def test_empty(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { @@ -182,7 +174,7 @@ def test_empty(self, mocked_db): ]) ) - def test_addition(self, mocked_db): + def test_addition(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { @@ -212,7 +204,7 @@ def test_addition(self, mocked_db): 'some query', ) - def test_override(self, mocked_db): + def test_override(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { diff --git a/tests/unit/test_sessions.py b/tests/unit/test_sessions.py index bdeb57e..d275594 100644 --- a/tests/unit/test_sessions.py +++ b/tests/unit/test_sessions.py @@ -1,27 +1,23 @@ import hashlib import unittest +from unittest import mock import flask_phpbb3.sessions -from unittest import mock - class TestSession(unittest.TestCase): - def setUp(self): - # type: () -> None + def setUp(self) -> None: self.session = flask_phpbb3.sessions.PhpBB3Session() class TestSessionMutability(TestSession): - def test_main(self): - # type: () -> None + def test_main(self) -> None: self.assertFalse(self.session.modified) self.session['a_key'] = 'some_value' self.assertTrue(self.session.modified) - def test_double_set(self): - # type: () -> None + def test_double_set(self) -> None: self.assertFalse(self.session.modified) self.session['a_key'] = 'some_value' @@ -29,15 +25,13 @@ def test_double_set(self): self.session['a_key'] = 'some_value' self.assertTrue(self.session.modified) - def test_same_value(self): - # type: () -> None + def test_same_value(self) -> None: self.assertFalse(self.session.modified) self.session['a_key'] = None # type: ignore self.assertFalse(self.session.modified) - def test_deletion(self): - # type: () -> None + def test_deletion(self) -> None: self.session['a_key'] = 'some_value' self.session.modified = False @@ -46,8 +40,7 @@ def test_deletion(self): del self.session['a_key'] self.assertTrue(self.session.modified) - def test_pop(self): - # type: () -> None + def test_pop(self) -> None: self.session['a_key'] = 'some_value' self.session.modified = False self.assertFalse(self.session.modified) @@ -56,16 +49,14 @@ def test_pop(self): self.assertEqual(actual_result, 'some_value') self.assertTrue(self.session.modified) - def test_read_only(self): - # type: () -> None + def test_read_only(self) -> None: self.session._read_only_properties.add('a_key') self.assertFalse(self.session.modified) self.session['a_key'] = 'some_value' self.assertFalse(self.session.modified) - def test_clear(self): - # type: () -> None + def test_clear(self) -> None: self.assertFalse(self.session.modified) self.session.clear() @@ -73,8 +64,7 @@ def test_clear(self): class TestSessionUser(TestSession): - def test_authenticated(self): - # type: () -> None + def test_authenticated(self) -> None: self.session['user_id'] = '1' self.assertFalse(self.session.is_authenticated) @@ -87,8 +77,7 @@ def test_authenticated(self): self.session['user_id'] = '-1' self.assertFalse(self.session.is_authenticated) - def test_get_link_hash(self): - # type: () -> None + def test_get_link_hash(self) -> None: some_link = '/my/link' self.session['user_form_salt'] = 'some_salt' @@ -97,13 +86,14 @@ def test_get_link_hash(self): self.session['user_id'] = '3' salted_link: str = self.session['user_form_salt'] + some_link - expected_value = hashlib.sha1(salted_link.encode('utf-8')).hexdigest()[:8] + expected_value = hashlib.sha1( + salted_link.encode('utf-8') + ).hexdigest()[:8] self.assertEqual(self.session.get_link_hash(some_link), expected_value) class TestSessionUserMembership(TestSession): - def setUp(self): - # type: () -> None + def setUp(self) -> None: super(TestSessionUserMembership, self).setUp() self.user_id = 2 @@ -111,13 +101,11 @@ def setUp(self): self.session['user_id'] = self.user_id self.session['group_id'] = 2 - def test_default_group_id(self): - # type: () -> None + def test_default_group_id(self) -> None: self.session.is_member(self.group_id) @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') - def test_group_id(self, patched_phpbb3): - # type: (mock.Mock) -> None + def test_group_id(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership.return_value = True actual_result = self.session.is_member(5) @@ -128,8 +116,7 @@ def test_group_id(self, patched_phpbb3): ) @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') - def test_group_id_failed(self, patched_phpbb3): - # type: (mock.Mock) -> None + def test_group_id_failed(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership.return_value = False actual_result = self.session.is_member(5) @@ -140,8 +127,7 @@ def test_group_id_failed(self, patched_phpbb3): ) @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') - def test_group_name(self, patched_phpbb3): - # type: (mock.Mock) -> None + def test_group_name(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership_resolve.return_value = True actual_result = self.session.is_member('group') @@ -152,8 +138,7 @@ def test_group_name(self, patched_phpbb3): ) @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') - def test_group_name_failed(self, patched_phpbb3): - # type: (mock.Mock) -> None + def test_group_name_failed(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership_resolve.return_value = False actual_result = self.session.is_member('group') @@ -164,8 +149,7 @@ def test_group_name_failed(self, patched_phpbb3): ) @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') - def test_nones(self, patched_phpbb3): - # type: (mock.Mock) -> None + def test_nones(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership_resolve.return_value = None patched_phpbb3.has_membership.return_value = None @@ -178,33 +162,28 @@ def test_nones(self, patched_phpbb3): @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') class TestSessionUserPrivileges(TestSession): - def setUp(self): - # type: () -> None + def setUp(self) -> None: super(TestSessionUserPrivileges, self).setUp() self.session['user_permissions'] = '' - def test_load_data_privilege(self, mocked_phpbb3): - # type: (mock.Mock) -> None + def test_load_data_privilege(self, mocked_phpbb3: mock.Mock) -> None: self.session.has_privilege('m_view') mocked_phpbb3.get_user_acl.assert_called_once_with('') - def test_load_data_privileges(self, mocked_phpbb3): - # type: (mock.Mock) -> None + def test_load_data_privileges(self, mocked_phpbb3: mock.Mock) -> None: self.session.has_privileges('m_view', 'm_edit') mocked_phpbb3.get_user_acl.assert_called_once_with('') - def test_load_data_single(self, mocked_phpbb3): - # type: (mock.Mock) -> None + def test_load_data_single(self, mocked_phpbb3: mock.Mock) -> None: self.session.has_privilege('m_view') self.session.has_privileges('m_view', 'm_edit') mocked_phpbb3.get_user_acl.assert_called_once_with('') - def test_call_privilege(self, mocked_phpbb3): - # type: (mock.Mock) -> None + def test_call_privilege(self, mocked_phpbb3: mock.Mock) -> None: user_acl = mock.Mock() mocked_phpbb3.get_user_acl.return_value = user_acl forum_id = mock.Mock() @@ -213,8 +192,7 @@ def test_call_privilege(self, mocked_phpbb3): user_acl.has_privilege.assert_called_once_with('m_view', forum_id) - def test_call_privileges(self, mocked_phpbb3): - # type: (mock.Mock) -> None + def test_call_privileges(self, mocked_phpbb3: mock.Mock) -> None: user_acl = mock.Mock() mocked_phpbb3.get_user_acl.return_value = user_acl forum_id = mock.Mock() @@ -230,14 +208,12 @@ def test_call_privileges(self, mocked_phpbb3): @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') class TestUnreadNotificationsNum(TestSession): - def setUp(self): - # type: () -> None + def setUp(self) -> None: super(TestUnreadNotificationsNum, self).setUp() self.session['user_id'] = 2 - def test_main(self, mocked_phpbb3): - # type: (mock.Mock) -> None + def test_main(self, mocked_phpbb3: mock.Mock) -> None: mocked_phpbb3.get_unread_notifications_count.return_value = { 'num': 3, } @@ -245,15 +221,13 @@ def test_main(self, mocked_phpbb3): actual_result = self.session.num_unread_notifications self.assertEqual(actual_result, 3) - def test_none(self, mocked_phpbb3): - # type: (mock.Mock) -> None + def test_none(self, mocked_phpbb3: mock.Mock) -> None: mocked_phpbb3.get_unread_notifications_count.return_value = None actual_result = self.session.num_unread_notifications self.assertEqual(actual_result, 0) - def test_cache(self, mocked_phpbb3): - # type: (mock.Mock) -> None + def test_cache(self, mocked_phpbb3: mock.Mock) -> None: mocked_phpbb3.get_unread_notifications_count.return_value = { 'num': 3, } From 90be040dd2dfdb18e27571d1dfcec1b4eee60fc6 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Sat, 27 Dec 2025 14:10:32 +0100 Subject: [PATCH 17/22] Update package building --- .travis.yml | 9 ++------- pyproject.toml | 26 ++++++++++++++++++++++++++ setup.cfg | 24 ------------------------ setup.py | 4 ---- 4 files changed, 28 insertions(+), 35 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.travis.yml b/.travis.yml index a3d69f0..0f56b0c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,15 +4,10 @@ services: addons: postgresql: "9.2" python: - - "2.7" - - "3.6" + - "3.14" install: - | - if [[ $TRAVIS_PYTHON_VERSION = 3.14 ]]; then - pip install -r requirements/dev.txt - else - pip install -r requirements/dev-3.txt - fi + pip install -r requirements/dev-3.txt script: - if [[ $TRAVIS_PYTHON_VERSION = 3.14 ]]; then flake8; fi - if [[ $TRAVIS_PYTHON_VERSION != 3.14 ]]; then mypy flask_phpbb3; fi diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4b46a6a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "Flask-phpBB3" +version = "0.3.0" +authors = [ + { name="Gregor Kališnik", email="gregor@kalisnik.si" } +] +description = "Connector for Flask with phpBB3 board" +readme = "README.rst" +requires-python = ">=3.14" +classifiers = [ + "Environment :: Web Environment", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development :: Libraries :: Python Modules" +] +license = "BSD-3-Clause" +license-files = ["LICENSE"] + +[project.urls] +Homepage = "https://github.com/MasterMind2k/flask-phpbb3" +Issues = "https://github.com/Podnapisi-NET/flask-phpbb3/issues" + diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 173d176..0000000 --- a/setup.cfg +++ /dev/null @@ -1,24 +0,0 @@ -[metadata] -name = Flask-phpBB3 -author = Gregor Kalisnik -author_email = gregor@kalisnik.si -home-page = https://github.com/MasterMind2k/flask-phpbb3 -license = BSD -summary = Connector for Flask with phpBB3 board -description-file = README.rst -classifier = - Environment :: Web Environment - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.14 - Topic :: Internet :: WWW/HTTP :: Dynamic Content - Topic :: Software Development :: Libraries :: Python Modules - -[files] -packages = flask_phpbb3 - -[wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index e99cc4f..0000000 --- a/setup.py +++ /dev/null @@ -1,4 +0,0 @@ -import setuptools - - -setuptools.setup(setup_requires=['pbr'], pbr=True) From 84ff7238c1ce5e2b93c8e451044ce6984e3941d4 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Sat, 27 Dec 2025 21:23:28 +0100 Subject: [PATCH 18/22] Switch ordering of classes to make types Python 3.8 compatible, update imports --- flask_phpbb3/backends/base.py | 144 +++++++++++++++--------------- flask_phpbb3/backends/psycopg2.py | 8 +- flask_phpbb3/sessions.py | 8 +- 3 files changed, 81 insertions(+), 79 deletions(-) diff --git a/flask_phpbb3/backends/base.py b/flask_phpbb3/backends/base.py index 5403cb4..fd41e22 100644 --- a/flask_phpbb3/backends/base.py +++ b/flask_phpbb3/backends/base.py @@ -1,5 +1,5 @@ import abc -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union import cachelib @@ -7,77 +7,6 @@ ACL_OPTIONS_CACHE_TTL: int = 3600 * 1 -class BaseBackend: - KNOWN_OPERATIONS = ( - 'fetch', - 'get', - 'has', - 'set', - ) - KNOWN_DRIVERS = ( - 'psycopg2', - ) - - def __init__( - self, - cache: cachelib.BaseCache, - config: Dict[str, Any] - ): - self._functions: Dict[str, str] = {} - self._connection = None - self._cache = cache - self._config = config - - self._prepare_statements() - custom_statements = self._config.get('CUSTOM_STATEMENTS', {}) - if not isinstance(custom_statements, dict): - custom_statements = {} - self._functions.update(custom_statements) - - @abc.abstractmethod - def _setup_connection(self) -> None: - raise NotImplementedError - - @abc.abstractmethod - def _prepare_statements(self) -> None: - raise NotImplementedError - - @property - @abc.abstractmethod - def _db(self) -> Any: - raise NotImplementedError - - @abc.abstractmethod - def execute( - self, - command: str, - cache: bool = False, - cache_ttl: Optional[int] = None, - skip: int = 0, - limit: Optional[int] = 10, - **kwargs: int | str - ) -> Any: - raise NotImplementedError - - @abc.abstractmethod - def close(self) -> None: - raise NotImplementedError - - @property - @abc.abstractmethod - def is_closed(self) -> bool: - raise NotImplementedError - - def get_user_acl(self, raw_user_permissions: str) -> UserAcl: - raw_acl_options = self.execute( - 'fetch_acl_options', - cache=True, - cache_ttl=ACL_OPTIONS_CACHE_TTL, - limit=None, - ) - return UserAcl(raw_acl_options, raw_user_permissions) - - class UserAcl: def __init__(self, raw_acl_options: List[Dict], raw_user_permissions: str): self._acl_options = self._parse_acl_options(raw_acl_options) @@ -188,3 +117,74 @@ def has_privileges(self, *privileges: str, **kwargs: int) -> bool: for option in privileges: output |= self.has_privilege(option, forum_id=forum_id) return output + + +class BaseBackend: + KNOWN_OPERATIONS = ( + 'fetch', + 'get', + 'has', + 'set', + ) + KNOWN_DRIVERS = ( + 'psycopg2', + ) + + def __init__( + self, + cache: cachelib.BaseCache, + config: Dict[str, Any] + ): + self._functions: Dict[str, str] = {} + self._connection = None + self._cache = cache + self._config = config + + self._prepare_statements() + custom_statements = self._config.get('CUSTOM_STATEMENTS', {}) + if not isinstance(custom_statements, dict): + custom_statements = {} + self._functions.update(custom_statements) + + @abc.abstractmethod + def _setup_connection(self) -> None: + raise NotImplementedError + + @abc.abstractmethod + def _prepare_statements(self) -> None: + raise NotImplementedError + + @property + @abc.abstractmethod + def _db(self) -> Any: + raise NotImplementedError + + @abc.abstractmethod + def execute( + self, + command: str, + cache: bool = False, + cache_ttl: Optional[int] = None, + skip: int = 0, + limit: Optional[int] = 10, + **kwargs: Union[int, str] + ) -> Any: + raise NotImplementedError + + @abc.abstractmethod + def close(self) -> None: + raise NotImplementedError + + @property + @abc.abstractmethod + def is_closed(self) -> bool: + raise NotImplementedError + + def get_user_acl(self, raw_user_permissions: str) -> UserAcl: + raw_acl_options = self.execute( + 'fetch_acl_options', + cache=True, + cache_ttl=ACL_OPTIONS_CACHE_TTL, + limit=None, + ) + return UserAcl(raw_acl_options, raw_user_permissions) diff --git a/flask_phpbb3/backends/psycopg2.py b/flask_phpbb3/backends/psycopg2.py index a7a37ec..96e2618 100644 --- a/flask_phpbb3/backends/psycopg2.py +++ b/flask_phpbb3/backends/psycopg2.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union try: import psycopg2 @@ -128,7 +128,7 @@ def _sql_query( cache_ttl: Optional[int] = None, skip: int = 0, limit: Optional[int] = 10, - **kwargs: int | str + **kwargs: Union[int, str] ) -> Any: """Executes a query with values in kwargs.""" if operation not in self.KNOWN_OPERATIONS: @@ -180,7 +180,7 @@ def _execute_operation( self, operation: str, query: str, - params: Dict[str, str | int] + params: Dict[str, Union[str, int]] ) -> Any: cursor = self._db.cursor() @@ -214,7 +214,7 @@ def execute( cache_ttl: Optional[int] = None, skip: int = 0, limit: Optional[int] = 10, - **kwargs: int | str + **kwargs: Union[int, str] ) -> Any: cache_key_prefix: Optional[str] = None if cache: diff --git a/flask_phpbb3/sessions.py b/flask_phpbb3/sessions.py index aade079..2367361 100644 --- a/flask_phpbb3/sessions.py +++ b/flask_phpbb3/sessions.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import json -from typing import Any, Dict, Optional, Set +from typing import Any, Dict, Optional, Set, Union import cachelib @@ -30,7 +32,7 @@ def __init__(self) -> None: # request should not be executed multiple times self._request_cache: Dict[str, Any] = {} - def __setitem__(self, key: str, value: str | int) -> None: + def __setitem__(self, key: str, value: Union[str, int]) -> None: modified: bool = self.get(key) != value super(PhpBB3Session, self).__setitem__(key, value) if key not in self._read_only_properties: @@ -60,7 +62,7 @@ def is_authenticated(self) -> bool: user_id = int(self.get('user_id', 1)) return user_id > 1 - def is_member(self, group: int | str) -> bool: + def is_member(self, group: Union[int, str]) -> bool: """Tests if user is a member of specified group.""" if isinstance(group, int): # Try with default group From 6ff847b0a831cfab973aad1405332078c3b6bf29 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Sun, 28 Dec 2025 18:10:35 +0100 Subject: [PATCH 19/22] Switch to uv package management --- pyproject.toml | 13 +- requirements.txt | 4 - requirements/dev-3.txt | 2 - requirements/dev.txt | 4 - uv.lock | 1243 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1253 insertions(+), 13 deletions(-) delete mode 100644 requirements.txt delete mode 100644 requirements/dev-3.txt delete mode 100644 requirements/dev.txt create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index 4b46a6a..4fb3911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ ] description = "Connector for Flask with phpBB3 board" readme = "README.rst" -requires-python = ">=3.14" +requires-python = ">=3.8" classifiers = [ "Environment :: Web Environment", "Intended Audience :: Developers", @@ -14,13 +14,20 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.14", + "License :: OSI Approved :: BSD License", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules" ] -license = "BSD-3-Clause" -license-files = ["LICENSE"] +dependencies = [ + "psycopg2>=2.8.4", + "Flask>=2.3.0", + "cachelib>=0.11.0", + "setuptools" +] [project.urls] Homepage = "https://github.com/MasterMind2k/flask-phpbb3" Issues = "https://github.com/Podnapisi-NET/flask-phpbb3/issues" +[dependency-groups] +dev = ["black", "ruff", "mypy", "coverage"] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d337766..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -psycopg2>=2.4.5 -Flask==3.1.2 -cachelib==0.13.0 -setuptools>=80.9.0 \ No newline at end of file diff --git a/requirements/dev-3.txt b/requirements/dev-3.txt deleted file mode 100644 index 111e389..0000000 --- a/requirements/dev-3.txt +++ /dev/null @@ -1,2 +0,0 @@ --r dev.txt -mypy>=1.19.1 diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 7a9de80..0000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,4 +0,0 @@ --r ../requirements.txt -flake8 -flake8-import-order -coverage \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..dd87646 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1243 @@ +version = 1 +revision = 3 +requires-python = ">=3.8" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] + +[[package]] +name = "black" +version = "24.8.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "packaging", marker = "python_full_version < '3.9'" }, + { name = "pathspec", marker = "python_full_version < '3.9'" }, + { name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/b0/46fb0d4e00372f4a86a6f8efa3cb193c9f64863615e39010b1477e010578/black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", size = 644810, upload-time = "2024-08-02T17:43:18.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/6e/74e29edf1fba3887ed7066930a87f698ffdcd52c5dbc263eabb06061672d/black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", size = 1632092, upload-time = "2024-08-02T17:47:26.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/575cb6c3faee690b05c9d11ee2e8dba8fbd6d6c134496e644c1feb1b47da/black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", size = 1457529, upload-time = "2024-08-02T17:47:29.109Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/d34099e95c437b53d01c4aa37cf93944b233066eb034ccf7897fa4e5f286/black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", size = 1757443, upload-time = "2024-08-02T17:46:20.306Z" }, + { url = "https://files.pythonhosted.org/packages/87/a0/6d2e4175ef364b8c4b64f8441ba041ed65c63ea1db2720d61494ac711c15/black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", size = 1418012, upload-time = "2024-08-02T17:47:20.33Z" }, + { url = "https://files.pythonhosted.org/packages/08/a6/0a3aa89de9c283556146dc6dbda20cd63a9c94160a6fbdebaf0918e4a3e1/black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1", size = 1615080, upload-time = "2024-08-02T17:48:05.467Z" }, + { url = "https://files.pythonhosted.org/packages/db/94/b803d810e14588bb297e565821a947c108390a079e21dbdcb9ab6956cd7a/black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", size = 1438143, upload-time = "2024-08-02T17:47:30.247Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b5/f485e1bbe31f768e2e5210f52ea3f432256201289fd1a3c0afda693776b0/black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", size = 1738774, upload-time = "2024-08-02T17:46:17.837Z" }, + { url = "https://files.pythonhosted.org/packages/a8/69/a000fc3736f89d1bdc7f4a879f8aaf516fb03613bb51a0154070383d95d9/black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", size = 1427503, upload-time = "2024-08-02T17:46:22.654Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a8/05fb14195cfef32b7c8d4585a44b7499c2a4b205e1662c427b941ed87054/black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", size = 1646132, upload-time = "2024-08-02T17:49:52.843Z" }, + { url = "https://files.pythonhosted.org/packages/41/77/8d9ce42673e5cb9988f6df73c1c5c1d4e9e788053cccd7f5fb14ef100982/black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", size = 1448665, upload-time = "2024-08-02T17:47:54.479Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/eff1ddad2ce1d3cc26c162b3693043c6b6b575f538f602f26fe846dfdc75/black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", size = 1762458, upload-time = "2024-08-02T17:46:19.384Z" }, + { url = "https://files.pythonhosted.org/packages/28/ea/18b8d86a9ca19a6942e4e16759b2fa5fc02bbc0eb33c1b866fcd387640ab/black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", size = 1436109, upload-time = "2024-08-02T17:46:52.97Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d4/ae03761ddecc1a37d7e743b89cccbcf3317479ff4b88cfd8818079f890d0/black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", size = 1617322, upload-time = "2024-08-02T17:51:20.203Z" }, + { url = "https://files.pythonhosted.org/packages/14/4b/4dfe67eed7f9b1ddca2ec8e4418ea74f0d1dc84d36ea874d618ffa1af7d4/black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", size = 1442108, upload-time = "2024-08-02T17:50:40.824Z" }, + { url = "https://files.pythonhosted.org/packages/97/14/95b3f91f857034686cae0e73006b8391d76a8142d339b42970eaaf0416ea/black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", size = 1745786, upload-time = "2024-08-02T17:46:02.939Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/68b8883c8aa258a6dde958cd5bdfada8382bec47c5162f4a01e66d839af1/black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", size = 1426754, upload-time = "2024-08-02T17:46:38.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/b2/b3f24fdbb46f0e7ef6238e131f13572ee8279b70f237f221dd168a9dba1a/black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", size = 1631706, upload-time = "2024-08-02T17:49:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/d9/35/31010981e4a05202a84a3116423970fd1a59d2eda4ac0b3570fbb7029ddc/black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", size = 1457429, upload-time = "2024-08-02T17:49:12.764Z" }, + { url = "https://files.pythonhosted.org/packages/27/25/3f706b4f044dd569a20a4835c3b733dedea38d83d2ee0beb8178a6d44945/black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", size = 1756488, upload-time = "2024-08-02T17:46:08.067Z" }, + { url = "https://files.pythonhosted.org/packages/63/72/79375cd8277cbf1c5670914e6bd4c1b15dea2c8f8e906dc21c448d0535f0/black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", size = 1417721, upload-time = "2024-08-02T17:46:42.637Z" }, + { url = "https://files.pythonhosted.org/packages/27/1e/83fa8a787180e1632c3d831f7e58994d7aaf23a0961320d21e84f922f919/black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", size = 206504, upload-time = "2024-08-02T17:43:15.747Z" }, +] + +[[package]] +name = "black" +version = "25.11.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "mypy-extensions", marker = "python_full_version == '3.9.*'" }, + { name = "packaging", marker = "python_full_version == '3.9.*'" }, + { name = "pathspec", marker = "python_full_version == '3.9.*'" }, + { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "pytokens", marker = "python_full_version == '3.9.*'" }, + { name = "tomli", marker = "python_full_version == '3.9.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" }, + { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" }, + { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" }, + { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" }, + { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" }, + { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" }, + { url = "https://files.pythonhosted.org/packages/ad/47/3378d6a2ddefe18553d1115e36aea98f4a90de53b6a3017ed861ba1bd3bc/black-25.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a1d40348b6621cc20d3d7530a5b8d67e9714906dfd7346338249ad9c6cedf2b", size = 1772446, upload-time = "2025-11-10T02:02:16.181Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/0f00bfb3d1f7e05e25bfc7c363f54dc523bb6ba502f98f4ad3acf01ab2e4/black-25.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51c65d7d60bb25429ea2bf0731c32b2a2442eb4bd3b2afcb47830f0b13e58bfd", size = 1607983, upload-time = "2025-11-10T02:02:52.502Z" }, + { url = "https://files.pythonhosted.org/packages/99/fe/49b0768f8c9ae57eb74cc10a1f87b4c70453551d8ad498959721cc345cb7/black-25.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:936c4dd07669269f40b497440159a221ee435e3fddcf668e0c05244a9be71993", size = 1682481, upload-time = "2025-11-10T01:57:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/17/7e10ff1267bfa950cc16f0a411d457cdff79678fbb77a6c73b73a5317904/black-25.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:f42c0ea7f59994490f4dccd64e6b2dd49ac57c7c84f38b8faab50f8759db245c", size = 1363869, upload-time = "2025-11-10T01:58:24.608Z" }, + { url = "https://files.pythonhosted.org/packages/67/c0/cc865ce594d09e4cd4dfca5e11994ebb51604328489f3ca3ae7bb38a7db5/black-25.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35690a383f22dd3e468c85dc4b915217f87667ad9cce781d7b42678ce63c4170", size = 1771358, upload-time = "2025-11-10T02:03:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/37/77/4297114d9e2fd2fc8ab0ab87192643cd49409eb059e2940391e7d2340e57/black-25.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dae49ef7369c6caa1a1833fd5efb7c3024bb7e4499bf64833f65ad27791b1545", size = 1612902, upload-time = "2025-11-10T01:59:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/de/63/d45ef97ada84111e330b2b2d45e1dd163e90bd116f00ac55927fb6bf8adb/black-25.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bd4a22a0b37401c8e492e994bce79e614f91b14d9ea911f44f36e262195fdda", size = 1680571, upload-time = "2025-11-10T01:57:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4b/5604710d61cdff613584028b4cb4607e56e148801ed9b38ee7970799dab6/black-25.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:aa211411e94fdf86519996b7f5f05e71ba34835d8f0c0f03c00a26271da02664", size = 1382599, upload-time = "2025-11-10T01:57:57.427Z" }, + { url = "https://files.pythonhosted.org/packages/d5/9a/5b2c0e3215fe748fcf515c2dd34658973a1210bf610e24de5ba887e4f1c8/black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", size = 1743063, upload-time = "2025-11-10T02:02:43.175Z" }, + { url = "https://files.pythonhosted.org/packages/a1/20/245164c6efc27333409c62ba54dcbfbe866c6d1957c9a6c0647786e950da/black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", size = 1596867, upload-time = "2025-11-10T02:00:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/1a3859a7da205f3d50cf3a8bec6bdc551a91c33ae77a045bb24c1f46ab54/black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", size = 1655678, upload-time = "2025-11-10T01:57:09.028Z" }, + { url = "https://files.pythonhosted.org/packages/56/1a/6dec1aeb7be90753d4fcc273e69bc18bfd34b353223ed191da33f7519410/black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", size = 1347452, upload-time = "2025-11-10T01:57:01.871Z" }, + { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" }, +] + +[[package]] +name = "black" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pathspec", marker = "python_full_version >= '3.10'" }, + { name = "platformdirs", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytokens", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/d5/8d3145999d380e5d09bb00b0f7024bf0a8ccb5c07b5648e9295f02ec1d98/black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8", size = 1895720, upload-time = "2025-12-08T01:46:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/06/97/7acc85c4add41098f4f076b21e3e4e383ad6ed0a3da26b2c89627241fc11/black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a", size = 1727193, upload-time = "2025-12-08T01:52:26.674Z" }, + { url = "https://files.pythonhosted.org/packages/24/f0/fdf0eb8ba907ddeb62255227d29d349e8256ef03558fbcadfbc26ecfe3b2/black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea", size = 1774506, upload-time = "2025-12-08T01:46:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f5/9203a78efe00d13336786b133c6180a9303d46908a9aa72d1104ca214222/black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f", size = 1416085, upload-time = "2025-12-08T01:46:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/7a6090e6b081c3316282c05c546e76affdce7bf7a3b7d2c3a2a69438bd01/black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da", size = 1226038, upload-time = "2025-12-08T01:45:29.388Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/7ac0d0e1e0612788dbc48e62aef8a8e8feffac7eb3d787db4e43b8462fa8/black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", size = 1877003, upload-time = "2025-12-08T01:43:29.967Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dd/a237e9f565f3617a88b49284b59cbca2a4f56ebe68676c1aad0ce36a54a7/black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", size = 1712639, upload-time = "2025-12-08T01:52:46.756Z" }, + { url = "https://files.pythonhosted.org/packages/12/80/e187079df1ea4c12a0c63282ddd8b81d5107db6d642f7d7b75a6bcd6fc21/black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", size = 1758143, upload-time = "2025-12-08T01:45:29.137Z" }, + { url = "https://files.pythonhosted.org/packages/93/b5/3096ccee4f29dc2c3aac57274326c4d2d929a77e629f695f544e159bfae4/black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", size = 1420698, upload-time = "2025-12-08T01:45:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/39/f81c0ffbc25ffbe61c7d0385bf277e62ffc3e52f5ee668d7369d9854fadf/black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", size = 1229317, upload-time = "2025-12-08T01:46:35.606Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, + { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, + { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, + { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, +] + +[[package]] +name = "blinker" +version = "1.8.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/57/a6a1721eff09598fb01f3c7cda070c1b6a0f12d63c83236edf79a440abcc/blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83", size = 23161, upload-time = "2024-05-06T17:04:10.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/2a/10164ed1f31196a2f7f3799368a821765c62851ead0e630ab52b8e14b4d0/blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01", size = 9456, upload-time = "2024-05-06T17:04:08.444Z" }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, +] + +[[package]] +name = "cachelib" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/9f/d08899acf9e0158e465070f68cbd5388fde5ab73be004c93e9102c5bac99/cachelib-0.11.0.tar.gz", hash = "sha256:d7289c9460c5cd4ef2506643d3b93d0e3accdf0a2f5792e4a4c0a26352ca462e", size = 32406, upload-time = "2024-02-10T14:17:48.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/45/790ac956be52979a2f1a190966c093758c85255cf3f5de950c0bb0f08122/cachelib-0.11.0-py3-none-any.whl", hash = "sha256:bd20fb49eb8822d4dbec239555818fc5c6e39d6e132a382d63e4e174f35b5956", size = 18487, upload-time = "2024-02-10T14:17:46.804Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", + "python_full_version < '3.9'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, + { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, + { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, + { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, + { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, + { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, + { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, + { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, + { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, + { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, + { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, + { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, + { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, + { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, + { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, + { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, + { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, + { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, + { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, + { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/d9e3d554e38beea5a2e22178ddb16587dbcbe9a1ef3211f55733924bf7fa/coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", size = 206674, upload-time = "2024-08-04T19:44:47.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/ea/cab2dc248d9f45b2b7f9f1f596a4d75a435cb364437c61b51d2eb33ceb0e/coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", size = 207101, upload-time = "2024-08-04T19:44:49.32Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6f/f82f9a500c7c5722368978a5390c418d2a4d083ef955309a8748ecaa8920/coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", size = 236554, upload-time = "2024-08-04T19:44:51.631Z" }, + { url = "https://files.pythonhosted.org/packages/a6/94/d3055aa33d4e7e733d8fa309d9adf147b4b06a82c1346366fc15a2b1d5fa/coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", size = 234440, upload-time = "2024-08-04T19:44:53.464Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/885bcd787d9dd674de4a7d8ec83faf729534c63d05d51d45d4fa168f7102/coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", size = 235889, upload-time = "2024-08-04T19:44:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/f4/63/df50120a7744492710854860783d6819ff23e482dee15462c9a833cc428a/coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", size = 235142, upload-time = "2024-08-04T19:44:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5d/9d0acfcded2b3e9ce1c7923ca52ccc00c78a74e112fc2aee661125b7843b/coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", size = 233805, upload-time = "2024-08-04T19:44:59.033Z" }, + { url = "https://files.pythonhosted.org/packages/c4/56/50abf070cb3cd9b1dd32f2c88f083aab561ecbffbcd783275cb51c17f11d/coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", size = 234655, upload-time = "2024-08-04T19:45:01.398Z" }, + { url = "https://files.pythonhosted.org/packages/25/ee/b4c246048b8485f85a2426ef4abab88e48c6e80c74e964bea5cd4cd4b115/coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", size = 209296, upload-time = "2024-08-04T19:45:03.819Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/96cf86b70b69ea2b12924cdf7cabb8ad10e6130eab8d767a1099fbd2a44f/coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", size = 210137, upload-time = "2024-08-04T19:45:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, + { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, + { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, + { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, + { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" }, + { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" }, + { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" }, + { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" }, + { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" }, + { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" }, + { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" }, + { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" }, + { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" }, + { url = "https://files.pythonhosted.org/packages/9a/94/b765c1abcb613d103b64fcf10395f54d69b0ef8be6a0dd9c524384892cc7/coverage-7.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d", size = 218320, upload-time = "2025-09-21T20:01:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/72/4f/732fff31c119bb73b35236dd333030f32c4bfe909f445b423e6c7594f9a2/coverage-7.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b", size = 218575, upload-time = "2025-09-21T20:01:58.203Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/ae7e0af4b674be47566707777db1aa375474f02a1d64b9323e5813a6cdd5/coverage-7.10.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e", size = 249568, upload-time = "2025-09-21T20:01:59.748Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/8c6d22bf61921a59bce5471c2f1f7ac30cd4ac50aadde72b8c48d5727902/coverage-7.10.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b", size = 252174, upload-time = "2025-09-21T20:02:01.192Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/b6ea4f69bbb52dac0aebd62157ba6a9dddbfe664f5af8122dac296c3ee15/coverage-7.10.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49", size = 253447, upload-time = "2025-09-21T20:02:02.701Z" }, + { url = "https://files.pythonhosted.org/packages/f9/28/4831523ba483a7f90f7b259d2018fef02cb4d5b90bc7c1505d6e5a84883c/coverage-7.10.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911", size = 249779, upload-time = "2025-09-21T20:02:04.185Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/4331142bc98c10ca6436d2d620c3e165f31e6c58d43479985afce6f3191c/coverage-7.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0", size = 251604, upload-time = "2025-09-21T20:02:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/ce/60/bda83b96602036b77ecf34e6393a3836365481b69f7ed7079ab85048202b/coverage-7.10.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f", size = 249497, upload-time = "2025-09-21T20:02:07.619Z" }, + { url = "https://files.pythonhosted.org/packages/5f/af/152633ff35b2af63977edd835d8e6430f0caef27d171edf2fc76c270ef31/coverage-7.10.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c", size = 249350, upload-time = "2025-09-21T20:02:10.34Z" }, + { url = "https://files.pythonhosted.org/packages/9d/71/d92105d122bd21cebba877228990e1646d862e34a98bb3374d3fece5a794/coverage-7.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f", size = 251111, upload-time = "2025-09-21T20:02:12.122Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9e/9fdb08f4bf476c912f0c3ca292e019aab6712c93c9344a1653986c3fd305/coverage-7.10.7-cp313-cp313-win32.whl", hash = "sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698", size = 220746, upload-time = "2025-09-21T20:02:13.919Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b1/a75fd25df44eab52d1931e89980d1ada46824c7a3210be0d3c88a44aaa99/coverage-7.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843", size = 221541, upload-time = "2025-09-21T20:02:15.57Z" }, + { url = "https://files.pythonhosted.org/packages/14/3a/d720d7c989562a6e9a14b2c9f5f2876bdb38e9367126d118495b89c99c37/coverage-7.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546", size = 220170, upload-time = "2025-09-21T20:02:17.395Z" }, + { url = "https://files.pythonhosted.org/packages/bb/22/e04514bf2a735d8b0add31d2b4ab636fc02370730787c576bb995390d2d5/coverage-7.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c", size = 219029, upload-time = "2025-09-21T20:02:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/91128e099035ece15da3445d9015e4b4153a6059403452d324cbb0a575fa/coverage-7.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15", size = 219259, upload-time = "2025-09-21T20:02:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/8b/51/66420081e72801536a091a0c8f8c1f88a5c4bf7b9b1bdc6222c7afe6dc9b/coverage-7.10.7-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4", size = 260592, upload-time = "2025-09-21T20:02:22.313Z" }, + { url = "https://files.pythonhosted.org/packages/5d/22/9b8d458c2881b22df3db5bb3e7369e63d527d986decb6c11a591ba2364f7/coverage-7.10.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0", size = 262768, upload-time = "2025-09-21T20:02:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/f7/08/16bee2c433e60913c610ea200b276e8eeef084b0d200bdcff69920bd5828/coverage-7.10.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0", size = 264995, upload-time = "2025-09-21T20:02:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/20/9d/e53eb9771d154859b084b90201e5221bca7674ba449a17c101a5031d4054/coverage-7.10.7-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65", size = 259546, upload-time = "2025-09-21T20:02:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b0/69bc7050f8d4e56a89fb550a1577d5d0d1db2278106f6f626464067b3817/coverage-7.10.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541", size = 262544, upload-time = "2025-09-21T20:02:29.216Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4b/2514b060dbd1bc0aaf23b852c14bb5818f244c664cb16517feff6bb3a5ab/coverage-7.10.7-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6", size = 260308, upload-time = "2025-09-21T20:02:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/7ba2175007c246d75e496f64c06e94122bdb914790a1285d627a918bd271/coverage-7.10.7-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999", size = 258920, upload-time = "2025-09-21T20:02:32.823Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/fac9f7abbc841409b9a410309d73bfa6cfb2e51c3fada738cb607ce174f8/coverage-7.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2", size = 261434, upload-time = "2025-09-21T20:02:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/a03bec00d37faaa891b3ff7387192cef20f01604e5283a5fabc95346befa/coverage-7.10.7-cp313-cp313t-win32.whl", hash = "sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a", size = 221403, upload-time = "2025-09-21T20:02:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/53/22/3cf25d614e64bf6d8e59c7c669b20d6d940bb337bdee5900b9ca41c820bb/coverage-7.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb", size = 222469, upload-time = "2025-09-21T20:02:39.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/a1/00164f6d30d8a01c3c9c48418a7a5be394de5349b421b9ee019f380df2a0/coverage-7.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb", size = 220731, upload-time = "2025-09-21T20:02:40.939Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/5844ab4ca6a4dd97a1850e030a15ec7d292b5c5cb93082979225126e35dd/coverage-7.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520", size = 218302, upload-time = "2025-09-21T20:02:42.527Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/673f6514b0961d1f0e20ddc242e9342f6da21eaba3489901b565c0689f34/coverage-7.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32", size = 218578, upload-time = "2025-09-21T20:02:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e8/261cae479e85232828fb17ad536765c88dd818c8470aca690b0ac6feeaa3/coverage-7.10.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f", size = 249629, upload-time = "2025-09-21T20:02:46.503Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/14ed6546d0207e6eda876434e3e8475a3e9adbe32110ce896c9e0c06bb9a/coverage-7.10.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a", size = 252162, upload-time = "2025-09-21T20:02:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/ff/49/07f00db9ac6478e4358165a08fb41b469a1b053212e8a00cb02f0d27a05f/coverage-7.10.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360", size = 253517, upload-time = "2025-09-21T20:02:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/a2/59/c5201c62dbf165dfbc91460f6dbbaa85a8b82cfa6131ac45d6c1bfb52deb/coverage-7.10.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69", size = 249632, upload-time = "2025-09-21T20:02:51.971Z" }, + { url = "https://files.pythonhosted.org/packages/07/ae/5920097195291a51fb00b3a70b9bbd2edbfe3c84876a1762bd1ef1565ebc/coverage-7.10.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14", size = 251520, upload-time = "2025-09-21T20:02:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/a815dde77a2981f5743a60b63df31cb322c944843e57dbd579326625a413/coverage-7.10.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe", size = 249455, upload-time = "2025-09-21T20:02:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/aa/99/f5cdd8421ea656abefb6c0ce92556709db2265c41e8f9fc6c8ae0f7824c9/coverage-7.10.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e", size = 249287, upload-time = "2025-09-21T20:02:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/c3/7a/e9a2da6a1fc5d007dd51fca083a663ab930a8c4d149c087732a5dbaa0029/coverage-7.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd", size = 250946, upload-time = "2025-09-21T20:02:59.431Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5b/0b5799aa30380a949005a353715095d6d1da81927d6dbed5def2200a4e25/coverage-7.10.7-cp314-cp314-win32.whl", hash = "sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2", size = 221009, upload-time = "2025-09-21T20:03:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/da/b0/e802fbb6eb746de006490abc9bb554b708918b6774b722bb3a0e6aa1b7de/coverage-7.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681", size = 221804, upload-time = "2025-09-21T20:03:03.4Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e8/71d0c8e374e31f39e3389bb0bd19e527d46f00ea8571ec7ec8fd261d8b44/coverage-7.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880", size = 220384, upload-time = "2025-09-21T20:03:05.111Z" }, + { url = "https://files.pythonhosted.org/packages/62/09/9a5608d319fa3eba7a2019addeacb8c746fb50872b57a724c9f79f146969/coverage-7.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63", size = 219047, upload-time = "2025-09-21T20:03:06.795Z" }, + { url = "https://files.pythonhosted.org/packages/f5/6f/f58d46f33db9f2e3647b2d0764704548c184e6f5e014bef528b7f979ef84/coverage-7.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2", size = 219266, upload-time = "2025-09-21T20:03:08.495Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/183ffc817ba68e0b443b8c934c8795553eb0c14573813415bd59941ee165/coverage-7.10.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d", size = 260767, upload-time = "2025-09-21T20:03:10.172Z" }, + { url = "https://files.pythonhosted.org/packages/0f/48/71a8abe9c1ad7e97548835e3cc1adbf361e743e9d60310c5f75c9e7bf847/coverage-7.10.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0", size = 262931, upload-time = "2025-09-21T20:03:11.861Z" }, + { url = "https://files.pythonhosted.org/packages/84/fd/193a8fb132acfc0a901f72020e54be5e48021e1575bb327d8ee1097a28fd/coverage-7.10.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699", size = 265186, upload-time = "2025-09-21T20:03:13.539Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8f/74ecc30607dd95ad50e3034221113ccb1c6d4e8085cc761134782995daae/coverage-7.10.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9", size = 259470, upload-time = "2025-09-21T20:03:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/79ff53a769f20d71b07023ea115c9167c0bb56f281320520cf64c5298a96/coverage-7.10.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f", size = 262626, upload-time = "2025-09-21T20:03:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/88/e2/dac66c140009b61ac3fc13af673a574b00c16efdf04f9b5c740703e953c0/coverage-7.10.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1", size = 260386, upload-time = "2025-09-21T20:03:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f1/f48f645e3f33bb9ca8a496bc4a9671b52f2f353146233ebd7c1df6160440/coverage-7.10.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0", size = 258852, upload-time = "2025-09-21T20:03:21.007Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3b/8442618972c51a7affeead957995cfa8323c0c9bcf8fa5a027421f720ff4/coverage-7.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399", size = 261534, upload-time = "2025-09-21T20:03:23.12Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/101f3fa3a45146db0cb03f5b4376e24c0aac818309da23e2de0c75295a91/coverage-7.10.7-cp314-cp314t-win32.whl", hash = "sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235", size = 221784, upload-time = "2025-09-21T20:03:24.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a1/74c51803fc70a8a40d7346660379e144be772bab4ac7bb6e6b905152345c/coverage-7.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d", size = 222905, upload-time = "2025-09-21T20:03:26.93Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/f116a6d2127df30bcafbceef0302d8a64ba87488bf6f73a6d8eebf060873/coverage-7.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a", size = 220922, upload-time = "2025-09-21T20:03:28.672Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" }, + { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" }, + { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" }, + { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" }, + { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" }, + { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" }, + { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" }, + { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" }, + { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" }, + { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" }, + { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" }, + { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" }, + { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" }, + { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" }, + { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" }, + { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" }, + { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" }, + { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" }, + { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" }, + { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" }, + { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" }, + { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" }, + { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" }, + { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" }, + { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a4/e98e689347a1ff1a7f67932ab535cef82eb5e78f32a9e4132e114bbb3a0a/coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78", size = 218951, upload-time = "2025-12-28T15:41:16.653Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/7cbfe2bdc6e2f03d6b240d23dc45fdaf3fd270aaf2d640be77b7f16989ab/coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b", size = 219325, upload-time = "2025-12-28T15:41:18.609Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/efdabdb4929487baeb7cb2a9f7dac457d9356f6ad1b255be283d58b16316/coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd", size = 250309, upload-time = "2025-12-28T15:41:20.629Z" }, + { url = "https://files.pythonhosted.org/packages/12/da/91a52516e9d5aea87d32d1523f9cdcf7a35a3b298e6be05d6509ba3cfab2/coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992", size = 252907, upload-time = "2025-12-28T15:41:22.257Z" }, + { url = "https://files.pythonhosted.org/packages/75/38/f1ea837e3dc1231e086db1638947e00d264e7e8c41aa8ecacf6e1e0c05f4/coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4", size = 254148, upload-time = "2025-12-28T15:41:23.87Z" }, + { url = "https://files.pythonhosted.org/packages/7f/43/f4f16b881aaa34954ba446318dea6b9ed5405dd725dd8daac2358eda869a/coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a", size = 250515, upload-time = "2025-12-28T15:41:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/84/34/8cba7f00078bd468ea914134e0144263194ce849ec3baad187ffb6203d1c/coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766", size = 252292, upload-time = "2025-12-28T15:41:28.459Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/cffac66c7652d84ee4ac52d3ccb94c015687d3b513f9db04bfcac2ac800d/coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4", size = 250242, upload-time = "2025-12-28T15:41:30.02Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/9a64d462263dde416f3c0067efade7b52b52796f489b1037a95b0dc389c9/coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398", size = 250068, upload-time = "2025-12-28T15:41:32.007Z" }, + { url = "https://files.pythonhosted.org/packages/69/c8/a8994f5fece06db7c4a97c8fc1973684e178599b42e66280dded0524ef00/coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784", size = 251846, upload-time = "2025-12-28T15:41:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f7/91fa73c4b80305c86598a2d4e54ba22df6bf7d0d97500944af7ef155d9f7/coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461", size = 221512, upload-time = "2025-12-28T15:41:35.519Z" }, + { url = "https://files.pythonhosted.org/packages/45/0b/0768b4231d5a044da8f75e097a8714ae1041246bb765d6b5563bab456735/coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500", size = 222321, upload-time = "2025-12-28T15:41:37.371Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b8/bdcb7253b7e85157282450262008f1366aa04663f3e3e4c30436f596c3e2/coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9", size = 220949, upload-time = "2025-12-28T15:41:39.553Z" }, + { url = "https://files.pythonhosted.org/packages/70/52/f2be52cc445ff75ea8397948c96c1b4ee14f7f9086ea62fc929c5ae7b717/coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc", size = 219643, upload-time = "2025-12-28T15:41:41.567Z" }, + { url = "https://files.pythonhosted.org/packages/47/79/c85e378eaa239e2edec0c5523f71542c7793fe3340954eafb0bc3904d32d/coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a", size = 219997, upload-time = "2025-12-28T15:41:43.418Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/b1ade8bfb653c0bbce2d6d6e90cc6c254cbb99b7248531cc76253cb4da6d/coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4", size = 261296, upload-time = "2025-12-28T15:41:45.207Z" }, + { url = "https://files.pythonhosted.org/packages/1f/af/ebf91e3e1a2473d523e87e87fd8581e0aa08741b96265730e2d79ce78d8d/coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6", size = 263363, upload-time = "2025-12-28T15:41:47.163Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/fb2423526d446596624ac7fde12ea4262e66f86f5120114c3cfd0bb2befa/coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1", size = 265783, upload-time = "2025-12-28T15:41:49.03Z" }, + { url = "https://files.pythonhosted.org/packages/9b/26/ef2adb1e22674913b89f0fe7490ecadcef4a71fa96f5ced90c60ec358789/coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd", size = 260508, upload-time = "2025-12-28T15:41:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7d/f0f59b3404caf662e7b5346247883887687c074ce67ba453ea08c612b1d5/coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c", size = 263357, upload-time = "2025-12-28T15:41:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b1/29896492b0b1a047604d35d6fa804f12818fa30cdad660763a5f3159e158/coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0", size = 260978, upload-time = "2025-12-28T15:41:54.589Z" }, + { url = "https://files.pythonhosted.org/packages/48/f2/971de1238a62e6f0a4128d37adadc8bb882ee96afbe03ff1570291754629/coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e", size = 259877, upload-time = "2025-12-28T15:41:56.263Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fc/0474efcbb590ff8628830e9aaec5f1831594874360e3251f1fdec31d07a3/coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53", size = 262069, upload-time = "2025-12-28T15:41:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/88/4f/3c159b7953db37a7b44c0eab8a95c37d1aa4257c47b4602c04022d5cb975/coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842", size = 222184, upload-time = "2025-12-28T15:41:59.763Z" }, + { url = "https://files.pythonhosted.org/packages/58/a5/6b57d28f81417f9335774f20679d9d13b9a8fb90cd6160957aa3b54a2379/coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2", size = 223250, upload-time = "2025-12-28T15:42:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/81/7c/160796f3b035acfbb58be80e02e484548595aa67e16a6345e7910ace0a38/coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09", size = 221521, upload-time = "2025-12-28T15:42:03.275Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/ba0e597560c6563fc0adb902fda6526df5d4aa73bb10adf0574d03bd2206/coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894", size = 218996, upload-time = "2025-12-28T15:42:04.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8e/764c6e116f4221dc7aa26c4061181ff92edb9c799adae6433d18eeba7a14/coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a", size = 219326, upload-time = "2025-12-28T15:42:06.691Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a6/6130dc6d8da28cdcbb0f2bf8865aeca9b157622f7c0031e48c6cf9a0e591/coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f", size = 250374, upload-time = "2025-12-28T15:42:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/82/2b/783ded568f7cd6b677762f780ad338bf4b4750205860c17c25f7c708995e/coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909", size = 252882, upload-time = "2025-12-28T15:42:10.515Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b2/9808766d082e6a4d59eb0cc881a57fc1600eb2c5882813eefff8254f71b5/coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4", size = 254218, upload-time = "2025-12-28T15:42:12.208Z" }, + { url = "https://files.pythonhosted.org/packages/44/ea/52a985bb447c871cb4d2e376e401116520991b597c85afdde1ea9ef54f2c/coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75", size = 250391, upload-time = "2025-12-28T15:42:14.21Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/125b36cc12310718873cfc8209ecfbc1008f14f4f5fa0662aa608e579353/coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9", size = 252239, upload-time = "2025-12-28T15:42:16.292Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/10c1c164950cade470107f9f14bbac8485f8fb8515f515fca53d337e4a7f/coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465", size = 250196, upload-time = "2025-12-28T15:42:18.54Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c6/cd860fac08780c6fd659732f6ced1b40b79c35977c1356344e44d72ba6c4/coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864", size = 250008, upload-time = "2025-12-28T15:42:20.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/a8c58d3d38f82a5711e1e0a67268362af48e1a03df27c03072ac30feefcf/coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9", size = 251671, upload-time = "2025-12-28T15:42:22.114Z" }, + { url = "https://files.pythonhosted.org/packages/f0/bc/fd4c1da651d037a1e3d53e8cb3f8182f4b53271ffa9a95a2e211bacc0349/coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5", size = 221777, upload-time = "2025-12-28T15:42:23.919Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/71acabdc8948464c17e90b5ffd92358579bd0910732c2a1c9537d7536aa6/coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a", size = 222592, upload-time = "2025-12-28T15:42:25.619Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/a6fb943081bb0cc926499c7907731a6dc9efc2cbdc76d738c0ab752f1a32/coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0", size = 221169, upload-time = "2025-12-28T15:42:27.629Z" }, + { url = "https://files.pythonhosted.org/packages/16/61/d5b7a0a0e0e40d62e59bc8c7aa1afbd86280d82728ba97f0673b746b78e2/coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a", size = 219730, upload-time = "2025-12-28T15:42:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2c/8881326445fd071bb49514d1ce97d18a46a980712b51fee84f9ab42845b4/coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6", size = 220001, upload-time = "2025-12-28T15:42:31.319Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d7/50de63af51dfa3a7f91cc37ad8fcc1e244b734232fbc8b9ab0f3c834a5cd/coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673", size = 261370, upload-time = "2025-12-28T15:42:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2c/d31722f0ec918fd7453b2758312729f645978d212b410cd0f7c2aed88a94/coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5", size = 263485, upload-time = "2025-12-28T15:42:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/2c114fa5c5fc08ba0777e4aec4c97e0b4a1afcb69c75f1f54cff78b073ab/coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d", size = 265890, upload-time = "2025-12-28T15:42:36.517Z" }, + { url = "https://files.pythonhosted.org/packages/65/d9/f0794aa1c74ceabc780fe17f6c338456bbc4e96bd950f2e969f48ac6fb20/coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8", size = 260445, upload-time = "2025-12-28T15:42:38.646Z" }, + { url = "https://files.pythonhosted.org/packages/49/23/184b22a00d9bb97488863ced9454068c79e413cb23f472da6cbddc6cfc52/coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486", size = 263357, upload-time = "2025-12-28T15:42:40.788Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bd/58af54c0c9199ea4190284f389005779d7daf7bf3ce40dcd2d2b2f96da69/coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564", size = 260959, upload-time = "2025-12-28T15:42:42.808Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2a/6839294e8f78a4891bf1df79d69c536880ba2f970d0ff09e7513d6e352e9/coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7", size = 259792, upload-time = "2025-12-28T15:42:44.818Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/528674d4623283310ad676c5af7414b9850ab6d55c2300e8aa4b945ec554/coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416", size = 262123, upload-time = "2025-12-28T15:42:47.108Z" }, + { url = "https://files.pythonhosted.org/packages/06/c5/8c0515692fb4c73ac379d8dc09b18eaf0214ecb76ea6e62467ba7a1556ff/coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f", size = 222562, upload-time = "2025-12-28T15:42:49.144Z" }, + { url = "https://files.pythonhosted.org/packages/05/0e/c0a0c4678cb30dac735811db529b321d7e1c9120b79bd728d4f4d6b010e9/coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79", size = 223670, upload-time = "2025-12-28T15:42:51.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/b177aa0011f354abf03a8f30a85032686d290fdeed4222b27d36b4372a50/coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4", size = 221707, upload-time = "2025-12-28T15:42:53.034Z" }, + { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" }, +] + +[[package]] +name = "flask" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker", version = "1.8.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "blinker", version = "1.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.3.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "importlib-metadata", version = "8.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "importlib-metadata", version = "8.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug", version = "3.0.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "werkzeug", version = "3.1.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/f4/22384e109d9b7e295b20580bc6af5f45b78730d03f41f826639a5c006d1a/Flask-2.3.0.tar.gz", hash = "sha256:4088b4986f13cae23e02081e67df7e4e434ec6f85a5238bce5e6db6d1de61e06", size = 686253, upload-time = "2023-04-25T18:41:57.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/dd/45df0b0b93f449cb217486136071294595b2d6f8840eaf38704773093a37/Flask-2.3.0-py3-none-any.whl", hash = "sha256:99a4889167c86fe57c4036825ebc77dc5dc3241bf0d80ea80ca941587d0d6a52", size = 96970, upload-time = "2023-04-25T18:41:55.795Z" }, +] + +[[package]] +name = "flask-phpbb3" +version = "0.3.0" +source = { virtual = "." } +dependencies = [ + { name = "cachelib" }, + { name = "flask" }, + { name = "psycopg2" }, + { name = "setuptools", version = "75.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "setuptools", version = "80.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] + +[package.dev-dependencies] +dev = [ + { name = "black", version = "24.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "black", version = "25.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "coverage", version = "7.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, + { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "mypy", version = "1.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "cachelib", specifier = ">=0.11.0" }, + { name = "flask", specifier = ">=2.3.0" }, + { name = "psycopg2", specifier = ">=2.8.4" }, + { name = "setuptools" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "black" }, + { name = "coverage" }, + { name = "mypy" }, + { name = "ruff" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "zipp", version = "3.20.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "zipp", version = "3.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.7.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/8a/071f6628363d83e803d4783e0cd24fb9c5b798164300fcfaaa47c30659c0/librt-0.7.5.tar.gz", hash = "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa", size = 145868, upload-time = "2025-12-25T03:53:16.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/f2/3248d8419db99ab80bb36266735d1241f766ad5fd993071211f789b618a5/librt-0.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81056e01bba1394f1d92904ec61a4078f66df785316275edbaf51d90da8c6e26", size = 54703, upload-time = "2025-12-25T03:51:48.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/30/7e179543dbcb1311f84b7e797658ad85cf2d4474c468f5dbafa13f2a98a5/librt-0.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7c72c8756eeb3aefb1b9e3dac7c37a4a25db63640cac0ab6fc18e91a0edf05a", size = 56660, upload-time = "2025-12-25T03:51:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/15/91/3ba03ac1ac1abd66757a134b3bd56d9674928b163d0e686ea065a2bbb92d/librt-0.7.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddc4a16207f88f9597b397fc1f60781266d13b13de922ff61c206547a29e4bbd", size = 161026, upload-time = "2025-12-25T03:51:51.021Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6e/b8365f547817d37b44c4be2ffa02630be995ef18be52d72698cecc3640c5/librt-0.7.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63055d3dda433ebb314c9f1819942f16a19203c454508fdb2d167613f7017169", size = 169530, upload-time = "2025-12-25T03:51:52.417Z" }, + { url = "https://files.pythonhosted.org/packages/63/6a/8442eb0b6933c651a06e1888f863971f3391cc11338fdaa6ab969f7d1eac/librt-0.7.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f85f9b5db87b0f52e53c68ad2a0c5a53e00afa439bd54a1723742a2b1021276", size = 183272, upload-time = "2025-12-25T03:51:53.713Z" }, + { url = "https://files.pythonhosted.org/packages/90/c4/b1166df6ef8e1f68d309f50bf69e8e750a5ea12fe7e2cf202c771ff359fc/librt-0.7.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c566a4672564c5d54d8ab65cdaae5a87ee14c1564c1a2ddc7a9f5811c750f023", size = 179040, upload-time = "2025-12-25T03:51:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/fc/30/8f3fd9fd975b16c37832d6c248b976d2a0e33f155063781e064f249b37f1/librt-0.7.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee15c2a190ef389f14928135c6fb2d25cd3fdb7887bfd9a7b444bbdc8c06b96", size = 173506, upload-time = "2025-12-25T03:51:56.407Z" }, + { url = "https://files.pythonhosted.org/packages/75/71/c3d4d5658f9849bf8e07ffba99f892d49a0c9a4001323ed610db72aedc82/librt-0.7.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:584cb3e605ec45ba350962cec853e17be0a25a772f21f09f1e422f7044ae2a7d", size = 193573, upload-time = "2025-12-25T03:51:57.949Z" }, + { url = "https://files.pythonhosted.org/packages/86/7c/c1c8a0116a2eed3d58c8946c589a8f9e1354b9b825cc92eba58bb15f6fb1/librt-0.7.5-cp310-cp310-win32.whl", hash = "sha256:9c08527055fbb03c641c15bbc5b79dd2942fb6a3bd8dabf141dd7e97eeea4904", size = 42603, upload-time = "2025-12-25T03:51:59.215Z" }, + { url = "https://files.pythonhosted.org/packages/1d/00/b52c77ca294247420020b829b70465c6e6f2b9d59ab21d8051aac20432da/librt-0.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:dd810f2d39c526c42ea205e0addad5dc08ef853c625387806a29d07f9d150d9b", size = 48977, upload-time = "2025-12-25T03:52:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/11/89/42b3ccb702a7e5f7a4cf2afc8a0a8f8c5e7d4b4d3a7c3de6357673dddddb/librt-0.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f952e1a78c480edee8fb43aa2bf2e84dcd46c917d44f8065b883079d3893e8fc", size = 54705, upload-time = "2025-12-25T03:52:01.433Z" }, + { url = "https://files.pythonhosted.org/packages/bb/90/c16970b509c3c448c365041d326eeef5aeb2abaed81eb3187b26a3cd13f8/librt-0.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75965c1f4efb7234ff52a58b729d245a21e87e4b6a26a0ec08052f02b16274e4", size = 56667, upload-time = "2025-12-25T03:52:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/da4bdf6c190503f4663fbb781dfae5564a2b1c3f39a2da8e1ac7536ac7bd/librt-0.7.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:732e0aa0385b59a1b2545159e781c792cc58ce9c134249233a7c7250a44684c4", size = 161705, upload-time = "2025-12-25T03:52:03.395Z" }, + { url = "https://files.pythonhosted.org/packages/fb/88/c5da8e1f5f22b23d56e1fbd87266799dcf32828d47bf69fabc6f9673c6eb/librt-0.7.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdde31759bd8888f3ef0eebda80394a48961328a17c264dce8cc35f4b9cde35d", size = 171029, upload-time = "2025-12-25T03:52:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/38/8a/8dfc00a6f1febc094ed9a55a448fc0b3a591b5dfd83be6cfd76d0910b1f0/librt-0.7.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3146d52465b3b6397d25d513f428cb421c18df65b7378667bb5f1e3cc45805", size = 184704, upload-time = "2025-12-25T03:52:05.887Z" }, + { url = "https://files.pythonhosted.org/packages/ad/57/65dec835ff235f431801064a3b41268f2f5ee0d224dc3bbf46d911af5c1a/librt-0.7.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29c8d2fae11d4379ea207ba7fc69d43237e42cf8a9f90ec6e05993687e6d648b", size = 180720, upload-time = "2025-12-25T03:52:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/1e/27/92033d169bbcaa0d9a2dd476c179e5171ec22ed574b1b135a3c6104fb7d4/librt-0.7.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb41f04046b4f22b1e7ba5ef513402cd2e3477ec610e5f92d38fe2bba383d419", size = 174538, upload-time = "2025-12-25T03:52:08.075Z" }, + { url = "https://files.pythonhosted.org/packages/44/5c/0127098743575d5340624d8d4ec508d4d5ff0877dcee6f55f54bf03e5ed0/librt-0.7.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bb7883c1e94ceb87c2bf81385266f032da09cd040e804cc002f2c9d6b842e2f", size = 195240, upload-time = "2025-12-25T03:52:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/47/0f/be028c3e906a8ee6d29a42fd362e6d57d4143057f2bc0c454d489a0f898b/librt-0.7.5-cp311-cp311-win32.whl", hash = "sha256:84d4a6b9efd6124f728558a18e79e7cc5c5d4efc09b2b846c910de7e564f5bad", size = 42941, upload-time = "2025-12-25T03:52:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3a/2f0ed57f4c3ae3c841780a95dfbea4cd811c6842d9ee66171ce1af606d25/librt-0.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab4b0d3bee6f6ff7017e18e576ac7e41a06697d8dea4b8f3ab9e0c8e1300c409", size = 49244, upload-time = "2025-12-25T03:52:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7c/d7932aedfa5a87771f9e2799e7185ec3a322f4a1f4aa87c234159b75c8c8/librt-0.7.5-cp311-cp311-win_arm64.whl", hash = "sha256:730be847daad773a3c898943cf67fb9845a3961d06fb79672ceb0a8cd8624cfa", size = 42614, upload-time = "2025-12-25T03:52:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/33/9d/cb0a296cee177c0fee7999ada1c1af7eee0e2191372058814a4ca6d2baf0/librt-0.7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba1077c562a046208a2dc6366227b3eeae8f2c2ab4b41eaf4fd2fa28cece4203", size = 55689, upload-time = "2025-12-25T03:52:14.041Z" }, + { url = "https://files.pythonhosted.org/packages/79/5c/d7de4d4228b74c5b81a3fbada157754bb29f0e1f8c38229c669a7f90422a/librt-0.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:654fdc971c76348a73af5240d8e2529265b9a7ba6321e38dd5bae7b0d4ab3abe", size = 57142, upload-time = "2025-12-25T03:52:15.336Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b2/5da779184aae369b69f4ae84225f63741662a0fe422e91616c533895d7a4/librt-0.7.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6b7b58913d475911f6f33e8082f19dd9b120c4f4a5c911d07e395d67b81c6982", size = 165323, upload-time = "2025-12-25T03:52:16.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/40/6d5abc15ab6cc70e04c4d201bb28baffff4cfb46ab950b8e90935b162d58/librt-0.7.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e0fd344bad57026a8f4ccfaf406486c2fc991838050c2fef156170edc3b775", size = 174218, upload-time = "2025-12-25T03:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d0/5239a8507e6117a3cb59ce0095bdd258bd2a93d8d4b819a506da06d8d645/librt-0.7.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46aa91813c267c3f60db75d56419b42c0c0b9748ec2c568a0e3588e543fb4233", size = 189007, upload-time = "2025-12-25T03:52:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/8eed1166ffddbb01c25363e4c4e655f4bac298debe9e5a2dcfaf942438a1/librt-0.7.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ddc0ab9dbc5f9ceaf2bf7a367bf01f2697660e908f6534800e88f43590b271db", size = 183962, upload-time = "2025-12-25T03:52:19.723Z" }, + { url = "https://files.pythonhosted.org/packages/a1/83/260e60aab2f5ccba04579c5c46eb3b855e51196fde6e2bcf6742d89140a8/librt-0.7.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a488908a470451338607650f1c064175094aedebf4a4fa37890682e30ce0b57", size = 177611, upload-time = "2025-12-25T03:52:21.18Z" }, + { url = "https://files.pythonhosted.org/packages/c4/36/6dcfed0df41e9695665462bab59af15b7ed2b9c668d85c7ebadd022cbb76/librt-0.7.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47fc52602ffc374e69bf1b76536dc99f7f6dd876bd786c8213eaa3598be030a", size = 199273, upload-time = "2025-12-25T03:52:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b7/157149c8cffae6bc4293a52e0267860cee2398cb270798d94f1c8a69b9ae/librt-0.7.5-cp312-cp312-win32.whl", hash = "sha256:cda8b025875946ffff5a9a7590bf9acde3eb02cb6200f06a2d3e691ef3d9955b", size = 43191, upload-time = "2025-12-25T03:52:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/197dfeb8d3bdeb0a5344d0d8b3077f183ba5e76c03f158126f6072730998/librt-0.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:b591c094afd0ffda820e931148c9e48dc31a556dc5b2b9b3cc552fa710d858e4", size = 49462, upload-time = "2025-12-25T03:52:24.637Z" }, + { url = "https://files.pythonhosted.org/packages/03/ea/052a79454cc52081dfaa9a1c4c10a529f7a6a6805b2fac5805fea5b25975/librt-0.7.5-cp312-cp312-win_arm64.whl", hash = "sha256:532ddc6a8a6ca341b1cd7f4d999043e4c71a212b26fe9fd2e7f1e8bb4e873544", size = 42830, upload-time = "2025-12-25T03:52:25.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9a/8f61e16de0ff76590af893cfb5b1aa5fa8b13e5e54433d0809c7033f59ed/librt-0.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a", size = 55750, upload-time = "2025-12-25T03:52:26.975Z" }, + { url = "https://files.pythonhosted.org/packages/05/7c/a8a883804851a066f301e0bad22b462260b965d5c9e7fe3c5de04e6f91f8/librt-0.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0", size = 57170, upload-time = "2025-12-25T03:52:27.948Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/b3b47facf5945be294cf8a835b03589f70ee0e791522f99ec6782ed738b3/librt-0.7.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5", size = 165834, upload-time = "2025-12-25T03:52:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b6/b26910cd0a4e43e5d02aacaaea0db0d2a52e87660dca08293067ee05601a/librt-0.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325", size = 174820, upload-time = "2025-12-25T03:52:30.463Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a3/81feddd345d4c869b7a693135a462ae275f964fcbbe793d01ea56a84c2ee/librt-0.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec", size = 189609, upload-time = "2025-12-25T03:52:31.492Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/31310796ef4157d1d37648bf4a3b84555319f14cee3e9bad7bdd7bfd9a35/librt-0.7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89", size = 184589, upload-time = "2025-12-25T03:52:32.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/22/da3900544cb0ac6ab7a2857850158a0a093b86f92b264aa6c4a4f2355ff3/librt-0.7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25", size = 178251, upload-time = "2025-12-25T03:52:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/78e02609846e78b9b8c8e361753b3dbac9a07e6d5b567fe518de9e074ab0/librt-0.7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b", size = 199852, upload-time = "2025-12-25T03:52:34.826Z" }, + { url = "https://files.pythonhosted.org/packages/2a/25/05706f6b346429c951582f1b3561f4d5e1418d0d7ba1a0c181237cd77b3b/librt-0.7.5-cp313-cp313-win32.whl", hash = "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee", size = 43250, upload-time = "2025-12-25T03:52:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/c38677278ac0b9ae1afc611382ef6c9ea87f52ad257bd3d8d65f0eacdc6a/librt-0.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e", size = 49421, upload-time = "2025-12-25T03:52:36.895Z" }, + { url = "https://files.pythonhosted.org/packages/c0/47/1d71113df4a81de5fdfbd3d7244e05d3d67e89f25455c3380ca50b92741e/librt-0.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45", size = 42827, upload-time = "2025-12-25T03:52:37.856Z" }, + { url = "https://files.pythonhosted.org/packages/97/ae/8635b4efdc784220f1378be640d8b1a794332f7f6ea81bb4859bf9d18aa7/librt-0.7.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2", size = 55191, upload-time = "2025-12-25T03:52:38.839Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/ed7ef6955dc2032af37db9b0b31cd5486a138aa792e1bb9e64f0f4950e27/librt-0.7.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f", size = 56894, upload-time = "2025-12-25T03:52:39.805Z" }, + { url = "https://files.pythonhosted.org/packages/24/f1/02921d4a66a1b5dcd0493b89ce76e2762b98c459fe2ad04b67b2ea6fdd39/librt-0.7.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6", size = 163726, upload-time = "2025-12-25T03:52:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/65/87/27df46d2756fcb7a82fa7f6ca038a0c6064c3e93ba65b0b86fbf6a4f76a2/librt-0.7.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361", size = 172470, upload-time = "2025-12-25T03:52:42.226Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a9/e65a35e5d423639f4f3d8e17301ff13cc41c2ff97677fe9c361c26dbfbb7/librt-0.7.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e", size = 186807, upload-time = "2025-12-25T03:52:43.688Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/ac68aa582a996b1241773bd419823290c42a13dc9f494704a12a17ddd7b6/librt-0.7.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2", size = 181810, upload-time = "2025-12-25T03:52:45.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/03f6717677f20acd2d690813ec2bbe12a2de305f32c61479c53f7b9413bc/librt-0.7.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760", size = 175599, upload-time = "2025-12-25T03:52:46.177Z" }, + { url = "https://files.pythonhosted.org/packages/01/d7/f976ff4c07c59b69bb5eec7e5886d43243075bbef834428124b073471c86/librt-0.7.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2", size = 196506, upload-time = "2025-12-25T03:52:47.327Z" }, + { url = "https://files.pythonhosted.org/packages/b7/74/004f068b8888e61b454568b5479f88018fceb14e511ac0609cccee7dd227/librt-0.7.5-cp314-cp314-win32.whl", hash = "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8", size = 39747, upload-time = "2025-12-25T03:52:48.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/b1/ea3ec8fcf5f0a00df21f08972af77ad799604a306db58587308067d27af8/librt-0.7.5-cp314-cp314-win_amd64.whl", hash = "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e", size = 45970, upload-time = "2025-12-25T03:52:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/5d/30/5e3fb7ac4614a50fc67e6954926137d50ebc27f36419c9963a94f931f649/librt-0.7.5-cp314-cp314-win_arm64.whl", hash = "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d", size = 39075, upload-time = "2025-12-25T03:52:50.395Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7f/0af0a9306a06c2aabee3a790f5aa560c50ec0a486ab818a572dd3db6c851/librt-0.7.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802", size = 57375, upload-time = "2025-12-25T03:52:51.439Z" }, + { url = "https://files.pythonhosted.org/packages/57/1f/c85e510baf6572a3d6ef40c742eacedc02973ed2acdb5dba2658751d9af8/librt-0.7.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4", size = 59234, upload-time = "2025-12-25T03:52:52.687Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/bb6535e4250cd18b88d6b18257575a0239fa1609ebba925f55f51ae08e8e/librt-0.7.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2", size = 183873, upload-time = "2025-12-25T03:52:53.705Z" }, + { url = "https://files.pythonhosted.org/packages/8e/49/ad4a138cca46cdaa7f0e15fa912ce3ccb4cc0d4090bfeb8ccc35766fa6d5/librt-0.7.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5", size = 194609, upload-time = "2025-12-25T03:52:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2d/3b3cb933092d94bb2c1d3c9b503d8775f08d806588c19a91ee4d1495c2a8/librt-0.7.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416", size = 206777, upload-time = "2025-12-25T03:52:55.969Z" }, + { url = "https://files.pythonhosted.org/packages/3a/52/6e7611d3d1347812233dabc44abca4c8065ee97b83c9790d7ecc3f782bc8/librt-0.7.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899", size = 203208, upload-time = "2025-12-25T03:52:57.036Z" }, + { url = "https://files.pythonhosted.org/packages/27/aa/466ae4654bd2d45903fbf180815d41e3ae8903e5a1861f319f73c960a843/librt-0.7.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7", size = 196698, upload-time = "2025-12-25T03:52:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/97/8f/424f7e4525bb26fe0d3e984d1c0810ced95e53be4fd867ad5916776e18a3/librt-0.7.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf", size = 217194, upload-time = "2025-12-25T03:52:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/9e/33/13a4cb798a171b173f3c94db23adaf13a417130e1493933dc0df0d7fb439/librt-0.7.5-cp314-cp314t-win32.whl", hash = "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d", size = 40282, upload-time = "2025-12-25T03:53:01.091Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/62b136301796399d65dad73b580f4509bcbd347dff885a450bff08e80cb6/librt-0.7.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d", size = 46764, upload-time = "2025-12-25T03:53:02.381Z" }, + { url = "https://files.pythonhosted.org/packages/49/cb/940431d9410fda74f941f5cd7f0e5a22c63be7b0c10fa98b2b7022b48cb1/librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1", size = 39728, upload-time = "2025-12-25T03:53:03.306Z" }, + { url = "https://files.pythonhosted.org/packages/49/9e/882da7f7cb8dd36a5ede60dd612c481f2e8322f84d4aaee46921e167877d/librt-0.7.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:df2e210400b28e50994477ebf82f055698c79797b6ee47a1669d383ca33263e1", size = 54841, upload-time = "2025-12-25T03:53:04.404Z" }, + { url = "https://files.pythonhosted.org/packages/79/f3/ccb73a438a7e8519859941766efcb3382592e3e187ec1730d08778ab9c2b/librt-0.7.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2cc7d187e8c6e9b7bdbefa9697ce897a704ea7a7ce844f2b4e0e2aa07ae51d3", size = 56804, upload-time = "2025-12-25T03:53:05.424Z" }, + { url = "https://files.pythonhosted.org/packages/7d/48/0b1d28f7627e1bdda82584974bb2def9cee9511ee3098772ba93c6677370/librt-0.7.5-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39183abee670bc37b85f11e86c44a9cad1ed6efa48b580083e89ecee13dd9717", size = 159681, upload-time = "2025-12-25T03:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/70/8e/cd2dc62b47c49c038728b2cedad54f4e73d371efbadbff9c22727ee0b94a/librt-0.7.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191cbd42660446d67cf7a95ac7bfa60f49b8b3b0417c64f216284a1d86fc9335", size = 168516, upload-time = "2025-12-25T03:53:07.506Z" }, + { url = "https://files.pythonhosted.org/packages/35/43/ad71af75a76a0756338497487664b2b309a24d8daf15fa4ff335c830ae3e/librt-0.7.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea1b60b86595a5dc1f57b44a801a1c4d8209c0a69518391d349973a4491408e6", size = 182230, upload-time = "2025-12-25T03:53:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b9/60b0821ca49998846279f977734c372619bd7d07c6ccbc52dc8f8b42a3db/librt-0.7.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:af69d9e159575e877c7546d1ee817b4ae089aa221dd1117e20c24ad8dc8659c7", size = 178270, upload-time = "2025-12-25T03:53:09.663Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/599d6167aa2ef5202eb9c63b4ab9730a348674bdafec6f470bfa31f7d5d5/librt-0.7.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0e2bf8f91093fac43e3eaebacf777f12fd539dce9ec5af3efc6d8424e96ccd49", size = 172568, upload-time = "2025-12-25T03:53:10.798Z" }, + { url = "https://files.pythonhosted.org/packages/5f/88/043a99e1453c82b501a44cc86e22788ef990eae57e4f2f5d75e2e11f47da/librt-0.7.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8dcae24de1bc9da93aa689cb6313c70e776d7cea2fcf26b9b6160fedfe6bd9af", size = 192747, upload-time = "2025-12-25T03:53:11.906Z" }, + { url = "https://files.pythonhosted.org/packages/4c/dc/85480f31db9049300bb757063f4364618400a5add4e1e45fa0fe055ab6f4/librt-0.7.5-cp39-cp39-win32.whl", hash = "sha256:cdb001a1a0e4f41e613bca2c0fc147fc8a7396f53fc94201cbfd8ec7cd69ca4b", size = 42552, upload-time = "2025-12-25T03:53:13.282Z" }, + { url = "https://files.pythonhosted.org/packages/bc/99/a743723826bf0e765eda50849c00f7c26102fcbd9a04e82adfd062ad2017/librt-0.7.5-cp39-cp39-win_amd64.whl", hash = "sha256:a9eacbf983319b26b5f340a2e0cd47ac1ee4725a7f3a72fd0f15063c934b69d6", size = 48959, upload-time = "2025-12-25T03:53:14.972Z" }, +] + +[[package]] +name = "markupsafe" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/54/ad5eb37bf9d51800010a74e4665425831a9db4e7c4e0fde4352e391e808e/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc", size = 18206, upload-time = "2024-02-02T16:30:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4a/a4d49415e600bacae038c67f9fecc1d5433b9d3c71a4de6f33537b89654c/MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5", size = 14079, upload-time = "2024-02-02T16:30:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7b/85681ae3c33c385b10ac0f8dd025c30af83c78cec1c37a6aa3b55e67f5ec/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46", size = 26620, upload-time = "2024-02-02T16:30:08.31Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/2b1b570f6b8b803cef5ac28fdf78c0da318916c7d2fe9402a84d591b394c/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f", size = 25818, upload-time = "2024-02-02T16:30:09.577Z" }, + { url = "https://files.pythonhosted.org/packages/29/fe/a36ba8c7ca55621620b2d7c585313efd10729e63ef81e4e61f52330da781/MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900", size = 25493, upload-time = "2024-02-02T16:30:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/9c60231cdfda003434e8bd27282b1f4e197ad5a710c14bee8bea8a9ca4f0/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff", size = 30630, upload-time = "2024-02-02T16:30:13.144Z" }, + { url = "https://files.pythonhosted.org/packages/65/dc/1510be4d179869f5dafe071aecb3f1f41b45d37c02329dfba01ff59e5ac5/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad", size = 29745, upload-time = "2024-02-02T16:30:14.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/39/8d845dd7d0b0613d86e0ef89549bfb5f61ed781f59af45fc96496e897f3a/MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd", size = 30021, upload-time = "2024-02-02T16:30:16.032Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/356a6f62e4f3c5fbf2602b4771376af22a3b16efa74eb8716fb4e328e01e/MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4", size = 16659, upload-time = "2024-02-02T16:30:17.079Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/acbf292615c65f0604a0c6fc402ce6d8c991276e16c80c46a8f758fbd30c/MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5", size = 17213, upload-time = "2024-02-02T16:30:18.251Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/291e55127bb2ae67c64d66cef01432b5933859dfb7d6949daa721b89d0b3/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f", size = 18219, upload-time = "2024-02-02T16:30:19.988Z" }, + { url = "https://files.pythonhosted.org/packages/6b/cb/aed7a284c00dfa7c0682d14df85ad4955a350a21d2e3b06d8240497359bf/MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2", size = 14098, upload-time = "2024-02-02T16:30:21.063Z" }, + { url = "https://files.pythonhosted.org/packages/1c/cf/35fe557e53709e93feb65575c93927942087e9b97213eabc3fe9d5b25a55/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced", size = 29014, upload-time = "2024-02-02T16:30:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/97/18/c30da5e7a0e7f4603abfc6780574131221d9148f323752c2755d48abad30/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5", size = 28220, upload-time = "2024-02-02T16:30:24.76Z" }, + { url = "https://files.pythonhosted.org/packages/0c/40/2e73e7d532d030b1e41180807a80d564eda53babaf04d65e15c1cf897e40/MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c", size = 27756, upload-time = "2024-02-02T16:30:25.877Z" }, + { url = "https://files.pythonhosted.org/packages/18/46/5dca760547e8c59c5311b332f70605d24c99d1303dd9a6e1fc3ed0d73561/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f", size = 33988, upload-time = "2024-02-02T16:30:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/27febe918ac36397919cd4a67d5579cbbfa8da027fa1238af6285bb368ea/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a", size = 32718, upload-time = "2024-02-02T16:30:28.111Z" }, + { url = "https://files.pythonhosted.org/packages/f8/81/56e567126a2c2bc2684d6391332e357589a96a76cb9f8e5052d85cb0ead8/MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f", size = 33317, upload-time = "2024-02-02T16:30:29.214Z" }, + { url = "https://files.pythonhosted.org/packages/00/0b/23f4b2470accb53285c613a3ab9ec19dc944eaf53592cb6d9e2af8aa24cc/MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906", size = 16670, upload-time = "2024-02-02T16:30:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a2/c78a06a9ec6d04b3445a949615c4c7ed86a0b2eb68e44e7541b9d57067cc/MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617", size = 17224, upload-time = "2024-02-02T16:30:32.09Z" }, + { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" }, + { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" }, + { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ff/2c942a82c35a49df5de3a630ce0a8456ac2969691b230e530ac12314364c/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a", size = 18192, upload-time = "2024-02-02T16:30:57.715Z" }, + { url = "https://files.pythonhosted.org/packages/4f/14/6f294b9c4f969d0c801a4615e221c1e084722ea6114ab2114189c5b8cbe0/MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46", size = 14072, upload-time = "2024-02-02T16:30:58.844Z" }, + { url = "https://files.pythonhosted.org/packages/81/d4/fd74714ed30a1dedd0b82427c02fa4deec64f173831ec716da11c51a50aa/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532", size = 26928, upload-time = "2024-02-02T16:30:59.922Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/50319665ce81bb10e90d1cf76f9e1aa269ea6f7fa30ab4521f14d122a3df/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab", size = 26106, upload-time = "2024-02-02T16:31:01.582Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/f2b0f675635b05f6afd5ea03c094557bdb8622fa8e673387444fe8d8e787/MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68", size = 25781, upload-time = "2024-02-02T16:31:02.71Z" }, + { url = "https://files.pythonhosted.org/packages/51/e0/393467cf899b34a9d3678e78961c2c8cdf49fb902a959ba54ece01273fb1/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0", size = 30518, upload-time = "2024-02-02T16:31:04.392Z" }, + { url = "https://files.pythonhosted.org/packages/f6/02/5437e2ad33047290dafced9df741d9efc3e716b75583bbd73a9984f1b6f7/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4", size = 29669, upload-time = "2024-02-02T16:31:05.53Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7d/968284145ffd9d726183ed6237c77938c021abacde4e073020f920e060b2/MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3", size = 29933, upload-time = "2024-02-02T16:31:06.636Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f3/ecb00fc8ab02b7beae8699f34db9357ae49d9f21d4d3de6f305f34fa949e/MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff", size = 16656, upload-time = "2024-02-02T16:31:07.767Z" }, + { url = "https://files.pythonhosted.org/packages/92/21/357205f03514a49b293e214ac39de01fadd0970a6e05e4bf1ddd0ffd0881/MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029", size = 17206, upload-time = "2024-02-02T16:31:08.843Z" }, + { url = "https://files.pythonhosted.org/packages/0f/31/780bb297db036ba7b7bbede5e1d7f1e14d704ad4beb3ce53fb495d22bc62/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf", size = 18193, upload-time = "2024-02-02T16:31:10.155Z" }, + { url = "https://files.pythonhosted.org/packages/6c/77/d77701bbef72892affe060cdacb7a2ed7fd68dae3b477a8642f15ad3b132/MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2", size = 14073, upload-time = "2024-02-02T16:31:11.442Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a7/1e558b4f78454c8a3a0199292d96159eb4d091f983bc35ef258314fe7269/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8", size = 26486, upload-time = "2024-02-02T16:31:12.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5a/360da85076688755ea0cceb92472923086993e86b5613bbae9fbc14136b0/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3", size = 25685, upload-time = "2024-02-02T16:31:13.726Z" }, + { url = "https://files.pythonhosted.org/packages/6a/18/ae5a258e3401f9b8312f92b028c54d7026a97ec3ab20bfaddbdfa7d8cce8/MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465", size = 25338, upload-time = "2024-02-02T16:31:14.812Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cc/48206bd61c5b9d0129f4d75243b156929b04c94c09041321456fd06a876d/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e", size = 30439, upload-time = "2024-02-02T16:31:15.946Z" }, + { url = "https://files.pythonhosted.org/packages/d1/06/a41c112ab9ffdeeb5f77bc3e331fdadf97fa65e52e44ba31880f4e7f983c/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea", size = 29531, upload-time = "2024-02-02T16:31:17.13Z" }, + { url = "https://files.pythonhosted.org/packages/02/8c/ab9a463301a50dab04d5472e998acbd4080597abc048166ded5c7aa768c8/MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6", size = 29823, upload-time = "2024-02-02T16:31:18.247Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/9bc18da763496b055d8e98ce476c8e718dcfd78157e17f555ce6dd7d0895/MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf", size = 16658, upload-time = "2024-02-02T16:31:19.583Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f8/4da07de16f10551ca1f640c92b5f316f9394088b183c6a57183df6de5ae4/MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5", size = 17211, upload-time = "2024-02-02T16:31:20.96Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" }, + { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" }, + { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" }, +] + +[[package]] +name = "mypy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "mypy-extensions", marker = "python_full_version < '3.9'" }, + { name = "tomli", marker = "python_full_version < '3.9'" }, + { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051, upload-time = "2024-12-30T16:39:07.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb", size = 11211002, upload-time = "2024-12-30T16:37:22.435Z" }, + { url = "https://files.pythonhosted.org/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0", size = 10358400, upload-time = "2024-12-30T16:37:53.526Z" }, + { url = "https://files.pythonhosted.org/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d", size = 12095172, upload-time = "2024-12-30T16:37:50.332Z" }, + { url = "https://files.pythonhosted.org/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b", size = 12828732, upload-time = "2024-12-30T16:37:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427", size = 13012197, upload-time = "2024-12-30T16:38:05.037Z" }, + { url = "https://files.pythonhosted.org/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f", size = 9780836, upload-time = "2024-12-30T16:37:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c", size = 11120432, upload-time = "2024-12-30T16:37:11.533Z" }, + { url = "https://files.pythonhosted.org/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1", size = 10279515, upload-time = "2024-12-30T16:37:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8", size = 12025791, upload-time = "2024-12-30T16:36:58.73Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f", size = 12749203, upload-time = "2024-12-30T16:37:03.741Z" }, + { url = "https://files.pythonhosted.org/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1", size = 12885900, upload-time = "2024-12-30T16:37:57.948Z" }, + { url = "https://files.pythonhosted.org/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae", size = 9777869, upload-time = "2024-12-30T16:37:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668, upload-time = "2024-12-30T16:38:02.211Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060, upload-time = "2024-12-30T16:37:46.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167, upload-time = "2024-12-30T16:37:43.534Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341, upload-time = "2024-12-30T16:37:36.249Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991, upload-time = "2024-12-30T16:37:06.743Z" }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016, upload-time = "2024-12-30T16:37:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097, upload-time = "2024-12-30T16:37:25.144Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728, upload-time = "2024-12-30T16:38:08.634Z" }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965, upload-time = "2024-12-30T16:38:12.132Z" }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660, upload-time = "2024-12-30T16:38:17.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198, upload-time = "2024-12-30T16:38:32.839Z" }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276, upload-time = "2024-12-30T16:38:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31", size = 11143050, upload-time = "2024-12-30T16:38:29.743Z" }, + { url = "https://files.pythonhosted.org/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6", size = 10321087, upload-time = "2024-12-30T16:38:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319", size = 12066766, upload-time = "2024-12-30T16:38:47.038Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac", size = 12787111, upload-time = "2024-12-30T16:39:02.444Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b", size = 12974331, upload-time = "2024-12-30T16:38:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837", size = 9763210, upload-time = "2024-12-30T16:38:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35", size = 11200493, upload-time = "2024-12-30T16:38:26.935Z" }, + { url = "https://files.pythonhosted.org/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc", size = 10357702, upload-time = "2024-12-30T16:38:50.623Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9", size = 12091104, upload-time = "2024-12-30T16:38:53.735Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb", size = 12830167, upload-time = "2024-12-30T16:38:56.437Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60", size = 13013834, upload-time = "2024-12-30T16:38:59.204Z" }, + { url = "https://files.pythonhosted.org/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c", size = 9781231, upload-time = "2024-12-30T16:39:05.124Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905, upload-time = "2024-12-30T16:38:42.021Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "librt", marker = "python_full_version >= '3.9' and platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions", marker = "python_full_version >= '3.9'" }, + { name = "pathspec", marker = "python_full_version >= '3.9'" }, + { name = "tomli", marker = "python_full_version >= '3.9' and python_full_version < '3.11'" }, + { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[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, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "psycopg2" +version = "2.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/d7/6a93c99b5ba4d4d22daa3928b983cec66df4536ca50b22ce5dcac65e4e71/psycopg2-2.8.4.tar.gz", hash = "sha256:f898e5cc0a662a9e12bde6f931263a1bbd350cfb18e1d5336a12927851825bb6", size = 377883, upload-time = "2019-10-20T09:15:24.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/ad/327e9c1e09b91e57294eb9a38492fcb000f15c028a1b716714fee5be802e/psycopg2-2.8.4-cp38-cp38-win32.whl", hash = "sha256:4212ca404c4445dc5746c0d68db27d2cbfb87b523fe233dc84ecd24062e35677", size = 986329, upload-time = "2019-11-10T21:37:47.632Z" }, + { url = "https://files.pythonhosted.org/packages/5f/00/c2f699fd5848ffaf30affc82c5dc8966dd783eac58af70045aecf0facf13/psycopg2-2.8.4-cp38-cp38-win_amd64.whl", hash = "sha256:92a07dfd4d7c325dd177548c4134052d4842222833576c8391aab6f74038fc3f", size = 1130353, upload-time = "2019-11-10T21:37:52.106Z" }, +] + +[[package]] +name = "pytokens" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "setuptools" +version = "75.3.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/01/771ea46cce201dd42cff043a5eea929d1c030fb3d1c2ee2729d02ca7814c/setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5", size = 1354489, upload-time = "2025-03-12T00:02:19.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/65/3f0dba35760d902849d39d38c0a72767794b1963227b69a587f8a336d08c/setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9", size = 1251198, upload-time = "2025-03-12T00:02:17.554Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.0.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +dependencies = [ + { name = "markupsafe", version = "2.1.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/f9/0ba83eaa0df9b9e9d1efeb2ea351d0677c37d41ee5d0f91e98423c7281c9/werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d", size = 805170, upload-time = "2024-10-25T18:52:31.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/69/05837f91dfe42109203ffa3e488214ff86a6d68b2ed6c167da6cdc42349b/werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", size = 227979, upload-time = "2024-10-25T18:52:30.129Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version == '3.9.*'", +] +dependencies = [ + { name = "markupsafe", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, +] + +[[package]] +name = "zipp" +version = "3.20.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.9'", +] +sdist = { url = "https://files.pythonhosted.org/packages/54/bf/5c0000c44ebc80123ecbdddba1f5dcd94a5ada602a9c225d84b5aaa55e86/zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29", size = 24199, upload-time = "2024-09-13T13:44:16.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8b/5ba542fa83c90e09eac972fc9baca7a88e7e7ca4b221a89251954019308b/zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350", size = 9200, upload-time = "2024-09-13T13:44:14.38Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.9.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 3399d6680087972b13d414145bd35072d2fe819f Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Sun, 28 Dec 2025 18:42:01 +0100 Subject: [PATCH 20/22] Unify module imports --- flask_phpbb3/backends/base.py | 36 ++++++++------ flask_phpbb3/backends/psycopg2.py | 31 ++++++------ flask_phpbb3/extension.py | 83 ++++++++++++++++--------------- flask_phpbb3/sessions.py | 20 ++++---- 4 files changed, 88 insertions(+), 82 deletions(-) diff --git a/flask_phpbb3/backends/base.py b/flask_phpbb3/backends/base.py index fd41e22..8824e6d 100644 --- a/flask_phpbb3/backends/base.py +++ b/flask_phpbb3/backends/base.py @@ -1,5 +1,5 @@ import abc -from typing import Any, Dict, List, Optional, Union +import typing import cachelib @@ -8,18 +8,22 @@ class UserAcl: - def __init__(self, raw_acl_options: List[Dict], raw_user_permissions: str): + def __init__( + self, + raw_acl_options: typing.List[typing.Dict], + raw_user_permissions: str + ): self._acl_options = self._parse_acl_options(raw_acl_options) self._acl = self._parse_user_permissions(raw_user_permissions) - self._acl_lookup_cache: Dict[str, Any] = {} + self._acl_lookup_cache: typing.Dict[str, typing.Any] = {} @classmethod def _parse_acl_options( cls, - raw_acl_options: List[Dict[str, Any]] - ) -> Dict[str, Dict[str, int]]: + raw_acl_options: typing.List[typing.Dict[str, typing.Any]] + ) -> typing.Dict[str, typing.Dict[str, int]]: # Load ACL options, so we can decode the user ACL - acl_options: Dict[str, Dict[str, int]] = { + acl_options: typing.Dict[str, typing.Dict[str, int]] = { 'local': {}, 'global': {} } @@ -44,9 +48,9 @@ def _parse_acl_options( def _parse_user_permissions( cls, raw_user_permissions: str - ) -> Dict[str, str]: - seq_cache: Dict[str, str] = {} - acl: Dict[str, str] = {} + ) -> typing.Dict[str, str]: + seq_cache: typing.Dict[str, str] = {} + acl: typing.Dict[str, str] = {} split_user_permissions = raw_user_permissions\ .rstrip()\ @@ -133,9 +137,9 @@ class BaseBackend: def __init__( self, cache: cachelib.BaseCache, - config: Dict[str, Any] + config: typing.Dict[str, typing.Any] ): - self._functions: Dict[str, str] = {} + self._functions: typing.Dict[str, str] = {} self._connection = None self._cache = cache self._config = config @@ -156,7 +160,7 @@ def _prepare_statements(self) -> None: @property @abc.abstractmethod - def _db(self) -> Any: + def _db(self) -> typing.Any: raise NotImplementedError @abc.abstractmethod @@ -164,11 +168,11 @@ def execute( self, command: str, cache: bool = False, - cache_ttl: Optional[int] = None, + cache_ttl: typing.Optional[int] = None, skip: int = 0, - limit: Optional[int] = 10, - **kwargs: Union[int, str] - ) -> Any: + limit: typing.Optional[int] = 10, + **kwargs: typing.Union[int, str] + ) -> typing.Any: raise NotImplementedError @abc.abstractmethod diff --git a/flask_phpbb3/backends/psycopg2.py b/flask_phpbb3/backends/psycopg2.py index 96e2618..a68bbbc 100644 --- a/flask_phpbb3/backends/psycopg2.py +++ b/flask_phpbb3/backends/psycopg2.py @@ -1,5 +1,5 @@ import json -from typing import Any, Dict, List, Optional, Union +import typing try: import psycopg2 @@ -106,7 +106,8 @@ def _prepare_custom_fields_statements(self) -> None: Prepares statements for custom fields """ # Setters for custom fields - custom_fields: List[Any] = self._config.get('CUSTOM_USER_FIELDS', []) + custom_fields: typing.List[typing.Any] =\ + self._config.get('CUSTOM_USER_FIELDS', []) for custom_field in custom_fields: self._functions["set_{0}".format(custom_field)] = ( "UPDATE" @@ -124,12 +125,12 @@ def _sql_query( self, operation: str, query: str, - cache_key_prefix: Optional[str] = None, - cache_ttl: Optional[int] = None, + cache_key_prefix: typing.Optional[str] = None, + cache_ttl: typing.Optional[int] = None, skip: int = 0, - limit: Optional[int] = 10, - **kwargs: Union[int, str] - ) -> Any: + limit: typing.Optional[int] = 10, + **kwargs: typing.Union[int, str] + ) -> typing.Any: """Executes a query with values in kwargs.""" if operation not in self.KNOWN_OPERATIONS: raise ValueError("Unknown operation") @@ -169,7 +170,7 @@ def _paginate_query( self, query: str, skip: int, - limit: Optional[int] + limit: typing.Optional[int] ) -> str: output = query + ' OFFSET {:d}'.format(skip) if limit: @@ -180,8 +181,8 @@ def _execute_operation( self, operation: str, query: str, - params: Dict[str, Union[str, int]] - ) -> Any: + params: typing.Dict[str, typing.Union[str, int]] + ) -> typing.Any: cursor = self._db.cursor() cursor.execute( @@ -211,12 +212,12 @@ def execute( self, command: str, cache: bool = False, - cache_ttl: Optional[int] = None, + cache_ttl: typing.Optional[int] = None, skip: int = 0, - limit: Optional[int] = 10, - **kwargs: Union[int, str] - ) -> Any: - cache_key_prefix: Optional[str] = None + limit: typing.Optional[int] = 10, + **kwargs: typing.Union[int, str] + ) -> typing.Any: + cache_key_prefix: typing.Optional[str] = None if cache: cache_key_prefix = command diff --git a/flask_phpbb3/extension.py b/flask_phpbb3/extension.py index 615f885..ef58857 100644 --- a/flask_phpbb3/extension.py +++ b/flask_phpbb3/extension.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional +import typing import cachelib @@ -11,8 +11,8 @@ class PhpBB3: def __init__( self, - app: Optional[flask.Flask] = None, - cache: Optional[cachelib.BaseCache] = None, + app: typing.Optional[flask.Flask] = None, + cache: typing.Optional[cachelib.BaseCache] = None, ) -> None: self.app = app if app is not None: @@ -21,7 +21,7 @@ def __init__( def init_app( self, app: flask.Flask, - cache: Optional[cachelib.BaseCache] = None + cache: typing.Optional[cachelib.BaseCache] = None ) -> None: self._ensure_default_config(app) @@ -85,7 +85,7 @@ def _ensure_default_config(cls, app: flask.Flask) -> None: def _create_backend( cls, backend_type: str, - config: Dict[str, Any], + config: typing.Dict[str, typing.Any], cache: cachelib.BaseCache ) -> flask_phpbb3.backends.base.BaseBackend: if backend_type == 'psycopg2': @@ -102,7 +102,7 @@ def _backend(self) -> flask_phpbb3.backends.base.BaseBackend: """Returns phpbb3 backend""" current_app = self.app or flask.current_app - backend: Optional[flask_phpbb3.backends.base.BaseBackend] =\ + backend: typing.Optional[flask_phpbb3.backends.base.BaseBackend] =\ flask.g.get('phpbb3_backend', None) if backend is None or backend.is_closed: @@ -124,9 +124,9 @@ def get_autologin( self, key: str, cache: bool = False, - cache_ttl: Optional[int] = None - ) -> Optional[Dict]: - output: Optional[Dict] = self._backend.execute( + cache_ttl: typing.Optional[int] = None + ) -> typing.Optional[typing.Dict]: + output: typing.Optional[typing.Dict] = self._backend.execute( 'get_autologin', key=key, cache=cache, @@ -138,9 +138,9 @@ def get_session( self, session_id: str, cache: bool = False, - cache_ttl: Optional[int] = None - ) -> Optional[Dict]: - output: Optional[Dict] = self._backend.execute( + cache_ttl: typing.Optional[int] = None + ) -> typing.Optional[typing.Dict]: + output: typing.Optional[typing.Dict] = self._backend.execute( 'get_session', session_id=session_id, cache=cache, @@ -152,9 +152,9 @@ def get_user( self, user_id: int, cache: bool = False, - cache_ttl: Optional[int] = None - ) -> Optional[Dict]: - output: Optional[Dict] = self._backend.execute( + cache_ttl: typing.Optional[int] = None + ) -> typing.Optional[typing.Dict]: + output: typing.Optional[typing.Dict] = self._backend.execute( 'get_user', user_id=user_id, cache=cache, @@ -166,9 +166,9 @@ def get_user_profile( self, user_id: int, cache: bool = False, - cache_ttl: Optional[int] = None - ) -> Optional[Dict]: - output: Optional[Dict] = self._backend.execute( + cache_ttl: typing.Optional[int] = None + ) -> typing.Optional[typing.Dict]: + output: typing.Optional[typing.Dict] = self._backend.execute( 'get_user_profile', user_id=user_id, cache=cache, @@ -181,9 +181,9 @@ def has_membership( user_id: int, group_id: int, cache: bool = False, - cache_ttl: Optional[int] = None, - ) -> Optional[bool]: - output: Optional[bool] = self._backend.execute( + cache_ttl: typing.Optional[int] = None, + ) -> typing.Optional[bool]: + output: typing.Optional[bool] = self._backend.execute( 'has_membership', user_id=user_id, group_id=group_id, @@ -197,9 +197,9 @@ def has_membership_resolve( user_id: int, group_name: str, cache: bool = False, - cache_ttl: Optional[int] = None, - ) -> Optional[bool]: - output: Optional[bool] = self._backend.execute( + cache_ttl: typing.Optional[int] = None, + ) -> typing.Optional[bool]: + output: typing.Optional[bool] = self._backend.execute( 'has_membership_resolve', user_id=user_id, group_name=group_name, @@ -211,26 +211,27 @@ def has_membership_resolve( def fetch_acl_options( self, skip: int = 0, - limit: Optional[int] = 10, + limit: typing.Optional[int] = 10, cache: bool = False, - cache_ttl: Optional[int] = None, - ) -> Optional[List[Dict]]: - output: Optional[List[Dict]] = self._backend.execute( - 'fetch_acl_options', - skip=skip, - limit=limit, - cache=cache, - cache_ttl=cache_ttl, - ) + cache_ttl: typing.Optional[int] = None, + ) -> typing.Optional[typing.List[typing.Dict]]: + output: typing.Optional[typing.List[typing.Dict]] =\ + self._backend.execute( + 'fetch_acl_options', + skip=skip, + limit=limit, + cache=cache, + cache_ttl=cache_ttl, + ) return output def get_unread_notifications_count( self, user_id: int, cache: bool = False, - cache_ttl: Optional[int] = None, - ) -> Optional[Dict]: - output: Optional[Dict] = self._backend.execute( + cache_ttl: typing.Optional[int] = None, + ) -> typing.Optional[typing.Dict]: + output: typing.Optional[typing.Dict] = self._backend.execute( 'get_unread_notifications_count', user_id=user_id, cache=cache, @@ -248,9 +249,9 @@ def execute_custom( self, command: str, cache: bool = False, - cache_ttl: Optional[int] = None, - **kwargs: Any - ) -> Any: + cache_ttl: typing.Optional[int] = None, + **kwargs: typing.Any + ) -> typing.Any: output = self._backend.execute( command, cache=cache, @@ -259,7 +260,7 @@ def execute_custom( ) return output - def teardown(self, exception: Any) -> None: + def teardown(self, exception: typing.Any) -> None: backend: flask_phpbb3.backends.base.BaseBackend =\ flask.g.get('phpbb3_backend', None) if backend is not None: diff --git a/flask_phpbb3/sessions.py b/flask_phpbb3/sessions.py index 2367361..207d01a 100644 --- a/flask_phpbb3/sessions.py +++ b/flask_phpbb3/sessions.py @@ -1,7 +1,7 @@ from __future__ import annotations import json -from typing import Any, Dict, Optional, Set, Union +import typing import cachelib @@ -17,22 +17,22 @@ # TODO: Inheriting Dict causes a type-checker error, possibly restructure -class PhpBB3Session(Dict[str, Any], flask.sessions.SessionMixin): +class PhpBB3Session(typing.Dict[str, typing.Any], flask.sessions.SessionMixin): def __init__(self) -> None: # Some session related variables self.modified: bool = False self.new: bool = False - self._read_only_properties: Set[str] = set([]) + self._read_only_properties: typing.Set[str] = set([]) # Some ACL related things - self._acl: Optional[flask_phpbb3.backends.base.UserAcl] = None + self._acl: typing.Optional[flask_phpbb3.backends.base.UserAcl] = None # Per request cache # This should not be cached into session, but per # request should not be executed multiple times - self._request_cache: Dict[str, Any] = {} + self._request_cache: typing.Dict[str, typing.Any] = {} - def __setitem__(self, key: str, value: Union[str, int]) -> None: + def __setitem__(self, key: str, value: typing.Union[str, int]) -> None: modified: bool = self.get(key) != value super(PhpBB3Session, self).__setitem__(key, value) if key not in self._read_only_properties: @@ -47,7 +47,7 @@ def _phpbb3(self) -> flask_phpbb3.PhpBB3: output: flask_phpbb3.PhpBB3 = getattr(flask.current_app, 'phpbb3') return output - def pop(self, *args: Any, **kwargs: Any) -> Any: + def pop(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: """Wrapper to set modified.""" self.modified = True return super(PhpBB3Session, self).pop(*args, **kwargs) @@ -62,7 +62,7 @@ def is_authenticated(self) -> bool: user_id = int(self.get('user_id', 1)) return user_id > 1 - def is_member(self, group: Union[int, str]) -> bool: + def is_member(self, group: typing.Union[int, str]) -> bool: """Tests if user is a member of specified group.""" if isinstance(group, int): # Try with default group @@ -161,12 +161,12 @@ def open_session( raise ValueError('App not properly configured, phpbb3 is missing!') phpbb3: flask_phpbb3.PhpBB3 = getattr(app, 'phpbb3') - session_id: Optional[str] = request.args.get('sid', type=str)\ + session_id: typing.Optional[str] = request.args.get('sid', type=str)\ or request.cookies.get(cookie_name + 'sid', None) if not session_id: session_id = None - user: Optional[Dict] = None + user: typing.Optional[typing.Dict] = None if self._is_bot(app, request): user = {'user_id': 1, 'username': 'Anonymous'} elif session_id: From 31818e01fbfd28fe6aac19324774c1cf009d141c Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Sun, 28 Dec 2025 18:52:41 +0100 Subject: [PATCH 21/22] Specify definite typing based on context, change inharitance to python in-built types --- flask_phpbb3/backends/psycopg2.py | 2 +- flask_phpbb3/sessions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask_phpbb3/backends/psycopg2.py b/flask_phpbb3/backends/psycopg2.py index a68bbbc..5453e1e 100644 --- a/flask_phpbb3/backends/psycopg2.py +++ b/flask_phpbb3/backends/psycopg2.py @@ -106,7 +106,7 @@ def _prepare_custom_fields_statements(self) -> None: Prepares statements for custom fields """ # Setters for custom fields - custom_fields: typing.List[typing.Any] =\ + custom_fields: typing.List[str] =\ self._config.get('CUSTOM_USER_FIELDS', []) for custom_field in custom_fields: self._functions["set_{0}".format(custom_field)] = ( diff --git a/flask_phpbb3/sessions.py b/flask_phpbb3/sessions.py index 207d01a..00ea94b 100644 --- a/flask_phpbb3/sessions.py +++ b/flask_phpbb3/sessions.py @@ -17,7 +17,7 @@ # TODO: Inheriting Dict causes a type-checker error, possibly restructure -class PhpBB3Session(typing.Dict[str, typing.Any], flask.sessions.SessionMixin): +class PhpBB3Session(dict, flask.sessions.SessionMixin): def __init__(self) -> None: # Some session related variables self.modified: bool = False From e7ecc93e6ad7d823846a210a928f7f245659d3e0 Mon Sep 17 00:00:00 2001 From: Gorazd Gorup Date: Sun, 28 Dec 2025 19:30:16 +0100 Subject: [PATCH 22/22] Format files with Black formatter --- flask_phpbb3/__init__.py | 4 +- flask_phpbb3/backends/base.py | 89 +++++------- flask_phpbb3/backends/psycopg2.py | 195 ++++++++++++------------- flask_phpbb3/extension.py | 144 ++++++++---------- flask_phpbb3/sessions.py | 109 +++++++------- tests/integration/base.py | 128 ++++++++-------- tests/integration/test_extension.py | 156 ++++++++++---------- tests/unit/backends/test_base.py | 146 +++++++------------ tests/unit/backends/test_psycopg2.py | 209 +++++++++++++-------------- tests/unit/test_sessions.py | 108 +++++++------- 10 files changed, 593 insertions(+), 695 deletions(-) diff --git a/flask_phpbb3/__init__.py b/flask_phpbb3/__init__.py index 9035f1a..8255364 100644 --- a/flask_phpbb3/__init__.py +++ b/flask_phpbb3/__init__.py @@ -4,6 +4,4 @@ from .extension import PhpBB3 -__all__ = ( - 'PhpBB3', -) +__all__ = ("PhpBB3",) diff --git a/flask_phpbb3/backends/base.py b/flask_phpbb3/backends/base.py index 8824e6d..d619366 100644 --- a/flask_phpbb3/backends/base.py +++ b/flask_phpbb3/backends/base.py @@ -9,9 +9,7 @@ class UserAcl: def __init__( - self, - raw_acl_options: typing.List[typing.Dict], - raw_user_permissions: str + self, raw_acl_options: typing.List[typing.Dict], raw_user_permissions: str ): self._acl_options = self._parse_acl_options(raw_acl_options) self._acl = self._parse_user_permissions(raw_user_permissions) @@ -19,25 +17,22 @@ def __init__( @classmethod def _parse_acl_options( - cls, - raw_acl_options: typing.List[typing.Dict[str, typing.Any]] + cls, raw_acl_options: typing.List[typing.Dict[str, typing.Any]] ) -> typing.Dict[str, typing.Dict[str, int]]: # Load ACL options, so we can decode the user ACL acl_options: typing.Dict[str, typing.Dict[str, int]] = { - 'local': {}, - 'global': {} + "local": {}, + "global": {}, } local_index: int = 0 global_index: int = 0 for opt in raw_acl_options or []: - if opt['is_local'] == 1: - acl_options['local'][opt['auth_option']] =\ - local_index + if opt["is_local"] == 1: + acl_options["local"][opt["auth_option"]] = local_index local_index += 1 - if opt['is_global'] == 1: - acl_options['global'][opt['auth_option']] =\ - global_index + if opt["is_global"] == 1: + acl_options["global"][opt["auth_option"]] = global_index global_index += 1 # TODO By looking phpbb3 code, here also comes translation # option <=> id @@ -46,29 +41,24 @@ def _parse_acl_options( @classmethod def _parse_user_permissions( - cls, - raw_user_permissions: str + cls, raw_user_permissions: str ) -> typing.Dict[str, str]: seq_cache: typing.Dict[str, str] = {} acl: typing.Dict[str, str] = {} - split_user_permissions = raw_user_permissions\ - .rstrip()\ - .splitlines() + split_user_permissions = raw_user_permissions.rstrip().splitlines() for forum_id, perms in enumerate(split_user_permissions): if not perms: continue # Do the conversion magic - acl[str(forum_id)] = '' - for sub in [perms[j:j + 6] for j in range(0, len(perms), 6)]: + acl[str(forum_id)] = "" + for sub in [perms[j : j + 6] for j in range(0, len(perms), 6)]: if sub in seq_cache: converted = seq_cache[sub] else: converted = bin(int(sub, 36))[2:] - converted = seq_cache[sub] = '0'\ - * (31 - len(converted))\ - + converted + converted = seq_cache[sub] = "0" * (31 - len(converted)) + converted acl[str(forum_id)] += converted @@ -78,7 +68,7 @@ def has_privilege(self, privilege: str, forum_id: int = 0) -> bool: # Parse negation str_forum_id = str(forum_id) - negated = privilege.startswith('!') + negated = privilege.startswith("!") if negated: option = privilege[1:] else: @@ -87,35 +77,28 @@ def has_privilege(self, privilege: str, forum_id: int = 0) -> bool: self._acl_lookup_cache.setdefault(str_forum_id, {})[option] = False # Global permissions - if option in self._acl_options['global']\ - and '0' in self._acl: + if option in self._acl_options["global"] and "0" in self._acl: try: - acl_option = self._acl_options['global'][option] - permission = self._acl['0'][acl_option] - self._acl_lookup_cache[str_forum_id][option] =\ - bool(int(permission)) + acl_option = self._acl_options["global"][option] + permission = self._acl["0"][acl_option] + self._acl_lookup_cache[str_forum_id][option] = bool(int(permission)) except IndexError: pass # Local permissions - if str_forum_id != '0'\ - and option in self._acl_options['local']: + if str_forum_id != "0" and option in self._acl_options["local"]: try: - acl_option = self._acl_options['local'][option] - permission =\ - self._acl.get(str_forum_id, '0' * 31)[acl_option] - self._acl_lookup_cache[str_forum_id][option] |=\ - bool(int(permission)) + acl_option = self._acl_options["local"][option] + permission = self._acl.get(str_forum_id, "0" * 31)[acl_option] + self._acl_lookup_cache[str_forum_id][option] |= bool(int(permission)) except IndexError: pass - output: bool = ( - negated ^ self._acl_lookup_cache[str_forum_id][option] - ) + output: bool = negated ^ self._acl_lookup_cache[str_forum_id][option] return output def has_privileges(self, *privileges: str, **kwargs: int) -> bool: - forum_id = kwargs.get('forum_id', 0) + forum_id = kwargs.get("forum_id", 0) output = False for option in privileges: @@ -125,27 +108,21 @@ def has_privileges(self, *privileges: str, **kwargs: int) -> bool: class BaseBackend: KNOWN_OPERATIONS = ( - 'fetch', - 'get', - 'has', - 'set', - ) - KNOWN_DRIVERS = ( - 'psycopg2', + "fetch", + "get", + "has", + "set", ) + KNOWN_DRIVERS = ("psycopg2",) - def __init__( - self, - cache: cachelib.BaseCache, - config: typing.Dict[str, typing.Any] - ): + def __init__(self, cache: cachelib.BaseCache, config: typing.Dict[str, typing.Any]): self._functions: typing.Dict[str, str] = {} self._connection = None self._cache = cache self._config = config self._prepare_statements() - custom_statements = self._config.get('CUSTOM_STATEMENTS', {}) + custom_statements = self._config.get("CUSTOM_STATEMENTS", {}) if not isinstance(custom_statements, dict): custom_statements = {} self._functions.update(custom_statements) @@ -171,7 +148,7 @@ def execute( cache_ttl: typing.Optional[int] = None, skip: int = 0, limit: typing.Optional[int] = 10, - **kwargs: typing.Union[int, str] + **kwargs: typing.Union[int, str], ) -> typing.Any: raise NotImplementedError @@ -186,7 +163,7 @@ def is_closed(self) -> bool: def get_user_acl(self, raw_user_permissions: str) -> UserAcl: raw_acl_options = self.execute( - 'fetch_acl_options', + "fetch_acl_options", cache=True, cache_ttl=ACL_OPTIONS_CACHE_TTL, limit=None, diff --git a/flask_phpbb3/backends/psycopg2.py b/flask_phpbb3/backends/psycopg2.py index 5453e1e..25f3c2c 100644 --- a/flask_phpbb3/backends/psycopg2.py +++ b/flask_phpbb3/backends/psycopg2.py @@ -5,6 +5,7 @@ import psycopg2 except ImportError: from psycopg2cffi import compat + compat.register() import psycopg2.extensions import psycopg2.extras @@ -15,11 +16,11 @@ class Psycopg2Backend(base.BaseBackend): def _setup_connection(self) -> None: self._connection = psycopg2.connect( - 'dbname={DATABASE}' - ' host={HOST}' - ' user={USER}' - ' password={PASSWORD}'.format(**self._config), - connection_factory=psycopg2.extras.DictConnection + "dbname={DATABASE}" + " host={HOST}" + " user={USER}" + " password={PASSWORD}".format(**self._config), + connection_factory=psycopg2.extras.DictConnection, ) @property @@ -33,69 +34,73 @@ def _prepare_statements(self) -> None: """ Initializes prepared SQL statements, depending on version of PHPBB3 """ - self._functions.update(dict( - get_autologin=( - "SELECT u.* " - "FROM {TABLE_PREFIX}users u," - " {TABLE_PREFIX}sessions_keys k " - # FIXME Make it prettier, USER_NORMAL and USER_FOUNDER - "WHERE u.user_type IN (0, 3)" - "AND k.user_id=u.user_id" - "AND k.key_id = %(key)s" - ), - get_session=( - "SELECT * " - "FROM {TABLE_PREFIX}sessions s, {TABLE_PREFIX}users u " - "WHERE s.session_id = %(session_id)s " - "AND s.session_user_id=u.user_id" - ), - get_user=( - "SELECT * " - "FROM {TABLE_PREFIX}users " - "WHERE user_id = %(user_id)s"), - get_user_profile=( - "SELECT * " - "FROM {TABLE_PREFIX}profile_fields_data " - "WHERE user_id = %(user_id)s"), - has_membership=( - "SELECT ug.group_id " - "FROM {TABLE_PREFIX}user_group ug " - "WHERE ug.user_id = %(user_id)s " - " AND ug.group_id = %(group_id)s " - " AND ug.user_pending=0 " - "LIMIT 1" - ), - has_membership_resolve=( - "SELECT ug.group_id " - "FROM {TABLE_PREFIX}user_group ug," - " {TABLE_PREFIX}groups g " - "WHERE ug.user_id = %(user_id)s " - " AND g.group_name = %(group_name)s " - " AND ug.group_id=g.group_id " - " AND ug.user_pending=0 " - "LIMIT 1" - ), - fetch_acl_options=( - "SELECT" - " *" - " FROM" - " {TABLE_PREFIX}acl_options" - " ORDER BY" - " auth_option_id" - ), - get_unread_notifications_count=( - "SELECT" - " COUNT(n.*) as num" - " FROM" - " {TABLE_PREFIX}notifications n," - " {TABLE_PREFIX}notification_types nt" - " WHERE" - " n.user_id = %(user_id)s " - " AND nt.notification_type_id=n.notification_type_id" - " AND nt.notification_type_enabled=1 " - " AND n.notification_read=0" - ), - )) + self._functions.update( + dict( + get_autologin=( + "SELECT u.* " + "FROM {TABLE_PREFIX}users u," + " {TABLE_PREFIX}sessions_keys k " + # FIXME Make it prettier, USER_NORMAL and USER_FOUNDER + "WHERE u.user_type IN (0, 3)" + "AND k.user_id=u.user_id" + "AND k.key_id = %(key)s" + ), + get_session=( + "SELECT * " + "FROM {TABLE_PREFIX}sessions s, {TABLE_PREFIX}users u " + "WHERE s.session_id = %(session_id)s " + "AND s.session_user_id=u.user_id" + ), + get_user=( + "SELECT * " + "FROM {TABLE_PREFIX}users " + "WHERE user_id = %(user_id)s" + ), + get_user_profile=( + "SELECT * " + "FROM {TABLE_PREFIX}profile_fields_data " + "WHERE user_id = %(user_id)s" + ), + has_membership=( + "SELECT ug.group_id " + "FROM {TABLE_PREFIX}user_group ug " + "WHERE ug.user_id = %(user_id)s " + " AND ug.group_id = %(group_id)s " + " AND ug.user_pending=0 " + "LIMIT 1" + ), + has_membership_resolve=( + "SELECT ug.group_id " + "FROM {TABLE_PREFIX}user_group ug," + " {TABLE_PREFIX}groups g " + "WHERE ug.user_id = %(user_id)s " + " AND g.group_name = %(group_name)s " + " AND ug.group_id=g.group_id " + " AND ug.user_pending=0 " + "LIMIT 1" + ), + fetch_acl_options=( + "SELECT" + " *" + " FROM" + " {TABLE_PREFIX}acl_options" + " ORDER BY" + " auth_option_id" + ), + get_unread_notifications_count=( + "SELECT" + " COUNT(n.*) as num" + " FROM" + " {TABLE_PREFIX}notifications n," + " {TABLE_PREFIX}notification_types nt" + " WHERE" + " n.user_id = %(user_id)s " + " AND nt.notification_type_id=n.notification_type_id" + " AND nt.notification_type_enabled=1 " + " AND n.notification_read=0" + ), + ) + ) self._prepare_custom_fields_statements() @@ -106,8 +111,7 @@ def _prepare_custom_fields_statements(self) -> None: Prepares statements for custom fields """ # Setters for custom fields - custom_fields: typing.List[str] =\ - self._config.get('CUSTOM_USER_FIELDS', []) + custom_fields: typing.List[str] = self._config.get("CUSTOM_USER_FIELDS", []) for custom_field in custom_fields: self._functions["set_{0}".format(custom_field)] = ( "UPDATE" @@ -117,7 +121,7 @@ def _prepare_custom_fields_statements(self) -> None: " WHERE" " user_id = %(user_id)s" ).format( - TABLE_PREFIX=self._config['TABLE_PREFIX'], + TABLE_PREFIX=self._config["TABLE_PREFIX"], field=custom_field, ) @@ -129,18 +133,17 @@ def _sql_query( cache_ttl: typing.Optional[int] = None, skip: int = 0, limit: typing.Optional[int] = 10, - **kwargs: typing.Union[int, str] + **kwargs: typing.Union[int, str], ) -> typing.Any: """Executes a query with values in kwargs.""" if operation not in self.KNOWN_OPERATIONS: raise ValueError("Unknown operation") cache_key = None - if cache_key_prefix and operation != 'set': - cache_key = '{name}:{arguments}'.format( + if cache_key_prefix and operation != "set": + cache_key = "{name}:{arguments}".format( name=cache_key_prefix, - arguments=':'.join(key + str(value) - for key, value in kwargs.items()) + arguments=":".join(key + str(value) for key, value in kwargs.items()), ) raw_data = self._cache.get(cache_key) if raw_data and isinstance(raw_data, (str, bytes)): @@ -150,16 +153,14 @@ def _sql_query( # Woops :S pass - if operation == 'fetch': + if operation == "fetch": query = self._paginate_query(query, skip, limit) output = self._execute_operation(operation, query, kwargs) if cache_key: try: - self._cache.set(cache_key, - json.dumps(output), - cache_ttl) + self._cache.set(cache_key, json.dumps(output), cache_ttl) except ValueError: # Woops :S pass @@ -167,38 +168,32 @@ def _sql_query( return output def _paginate_query( - self, - query: str, - skip: int, - limit: typing.Optional[int] + self, query: str, skip: int, limit: typing.Optional[int] ) -> str: - output = query + ' OFFSET {:d}'.format(skip) + output = query + " OFFSET {:d}".format(skip) if limit: - output += ' LIMIT {:d}'.format(limit) + output += " LIMIT {:d}".format(limit) return output def _execute_operation( self, operation: str, query: str, - params: typing.Dict[str, typing.Union[str, int]] + params: typing.Dict[str, typing.Union[str, int]], ) -> typing.Any: cursor = self._db.cursor() - cursor.execute( - query.format(TABLE_PREFIX=self._config['TABLE_PREFIX']), - params - ) + cursor.execute(query.format(TABLE_PREFIX=self._config["TABLE_PREFIX"]), params) - if operation == 'get': + if operation == "get": output = cursor.fetchone() if output is not None: output = dict(output) - elif operation == 'has': + elif operation == "has": output = bool(cursor.fetchone()) - elif operation == 'fetch': + elif operation == "fetch": output = [dict(i) for i in cursor] - elif operation == 'set': + elif operation == "set": # It is an update output = cursor.statusmessage self._db.commit() @@ -215,18 +210,16 @@ def execute( cache_ttl: typing.Optional[int] = None, skip: int = 0, limit: typing.Optional[int] = 10, - **kwargs: typing.Union[int, str] + **kwargs: typing.Union[int, str], ) -> typing.Any: cache_key_prefix: typing.Optional[str] = None if cache: cache_key_prefix = command if command not in self._functions: - raise ValueError("Function {} does not exist.".format( - command - )) + raise ValueError("Function {} does not exist.".format(command)) - operation = command.split('_')[0] + operation = command.split("_")[0] func_or_query = self._functions[command] if callable(func_or_query): @@ -239,7 +232,7 @@ def execute( cache_ttl=cache_ttl, skip=skip, limit=limit, - **kwargs + **kwargs, ) def close(self) -> None: diff --git a/flask_phpbb3/extension.py b/flask_phpbb3/extension.py index ef58857..60fbe2f 100644 --- a/flask_phpbb3/extension.py +++ b/flask_phpbb3/extension.py @@ -19,9 +19,7 @@ def __init__( self.init_app(app, cache) def init_app( - self, - app: flask.Flask, - cache: typing.Optional[cachelib.BaseCache] = None + self, app: flask.Flask, cache: typing.Optional[cachelib.BaseCache] = None ) -> None: self._ensure_default_config(app) @@ -29,11 +27,11 @@ def init_app( cache_driver: cachelib.BaseCache if not cache: # Setup our own - cache_backend = app.config['PHPBB3_SESSION_BACKEND']['TYPE'] - if cache_backend == 'memcached': - key_prefix = app.config['PHPBB3_SESSION_BACKEND']['KEY_PREFIX'] + cache_backend = app.config["PHPBB3_SESSION_BACKEND"]["TYPE"] + if cache_backend == "memcached": + key_prefix = app.config["PHPBB3_SESSION_BACKEND"]["KEY_PREFIX"] cache_driver = cachelib.MemcachedCache( - app.config['PHPBB3_SESSION_BACKEND']['SERVERS'], + app.config["PHPBB3_SESSION_BACKEND"]["SERVERS"], key_prefix=key_prefix, ) else: @@ -45,40 +43,35 @@ def init_app( app.teardown_appcontext(self.teardown) # Add ourselves to the app, so session interface can function - setattr(app, 'phpbb3', self) - setattr(app, 'phpbb3_cache', cache_driver) + setattr(app, "phpbb3", self) + setattr(app, "phpbb3_cache", cache_driver) # Use our session interface # TODO Is it wise to do it here? Should user do it himself? - app.session_interface =\ - flask_phpbb3.sessions.PhpBB3SessionInterface() + app.session_interface = flask_phpbb3.sessions.PhpBB3SessionInterface() @classmethod def _ensure_default_config(cls, app: flask.Flask) -> None: - app.config.setdefault('PHPBB3', {}) - app.config['PHPBB3'].setdefault('DRIVER', 'psycopg2') - app.config['PHPBB3'].setdefault('VERSION', '3.1') - app.config.setdefault('PHPBB3_DATABASE', {}) - app.config['PHPBB3_DATABASE'].setdefault('HOST', '127.0.0.1') - app.config['PHPBB3_DATABASE'].setdefault('DATABASE', 'phpbb3') - app.config['PHPBB3_DATABASE'].setdefault('USER', 'phpbb3') - app.config['PHPBB3_DATABASE'].setdefault('PASSWORD', '') - app.config['PHPBB3_DATABASE'].setdefault('TABLE_PREFIX', 'phpbb_') - app.config['PHPBB3_DATABASE'].setdefault('CUSTOM_USER_FIELDS', []) - app.config['PHPBB3_DATABASE'].setdefault('CUSTOM_STATEMENTS', {}) - app.config.setdefault('PHPBB3_SESSION_BACKEND', {}) - app.config['PHPBB3_SESSION_BACKEND'].setdefault('TYPE', 'simple') - app.config.setdefault('PHPBB3_BOTLIST', []) + app.config.setdefault("PHPBB3", {}) + app.config["PHPBB3"].setdefault("DRIVER", "psycopg2") + app.config["PHPBB3"].setdefault("VERSION", "3.1") + app.config.setdefault("PHPBB3_DATABASE", {}) + app.config["PHPBB3_DATABASE"].setdefault("HOST", "127.0.0.1") + app.config["PHPBB3_DATABASE"].setdefault("DATABASE", "phpbb3") + app.config["PHPBB3_DATABASE"].setdefault("USER", "phpbb3") + app.config["PHPBB3_DATABASE"].setdefault("PASSWORD", "") + app.config["PHPBB3_DATABASE"].setdefault("TABLE_PREFIX", "phpbb_") + app.config["PHPBB3_DATABASE"].setdefault("CUSTOM_USER_FIELDS", []) + app.config["PHPBB3_DATABASE"].setdefault("CUSTOM_STATEMENTS", {}) + app.config.setdefault("PHPBB3_SESSION_BACKEND", {}) + app.config["PHPBB3_SESSION_BACKEND"].setdefault("TYPE", "simple") + app.config.setdefault("PHPBB3_BOTLIST", []) # Conditional defaults - if app.config['PHPBB3_SESSION_BACKEND']['TYPE'] == 'memcached': - app.config['PHPBB3_SESSION_BACKEND'].setdefault( - 'KEY_PREFIX', - 'phpbb3' - ) - app.config['PHPBB3_SESSION_BACKEND'].setdefault( - 'SERVERS', - ['127.0.0.1:11211'] + if app.config["PHPBB3_SESSION_BACKEND"]["TYPE"] == "memcached": + app.config["PHPBB3_SESSION_BACKEND"].setdefault("KEY_PREFIX", "phpbb3") + app.config["PHPBB3_SESSION_BACKEND"].setdefault( + "SERVERS", ["127.0.0.1:11211"] ) @classmethod @@ -86,48 +79,47 @@ def _create_backend( cls, backend_type: str, config: typing.Dict[str, typing.Any], - cache: cachelib.BaseCache + cache: cachelib.BaseCache, ) -> flask_phpbb3.backends.base.BaseBackend: - if backend_type == 'psycopg2': + if backend_type == "psycopg2": import flask_phpbb3.backends.psycopg2 + return flask_phpbb3.backends.psycopg2.Psycopg2Backend( cache, config, ) else: - raise ValueError('Unsupported driver {}'.format(backend_type)) + raise ValueError("Unsupported driver {}".format(backend_type)) @property def _backend(self) -> flask_phpbb3.backends.base.BaseBackend: """Returns phpbb3 backend""" current_app = self.app or flask.current_app - backend: typing.Optional[flask_phpbb3.backends.base.BaseBackend] =\ - flask.g.get('phpbb3_backend', None) + backend: typing.Optional[flask_phpbb3.backends.base.BaseBackend] = flask.g.get( + "phpbb3_backend", None + ) if backend is None or backend.is_closed: - if not hasattr(current_app, 'phpbb3_cache'): + if not hasattr(current_app, "phpbb3_cache"): raise ValueError( - 'App not properly configured, phpbb3_cache is missing!' + "App not properly configured, phpbb3_cache is missing!" ) - cache = getattr(current_app, 'phpbb3_cache') + cache = getattr(current_app, "phpbb3_cache") backend = PhpBB3._create_backend( - current_app.config['PHPBB3']['DRIVER'], - current_app.config['PHPBB3_DATABASE'], + current_app.config["PHPBB3"]["DRIVER"], + current_app.config["PHPBB3_DATABASE"], cache, ) - flask.g.setdefault('phpbb3_backend', backend) + flask.g.setdefault("phpbb3_backend", backend) return backend def get_autologin( - self, - key: str, - cache: bool = False, - cache_ttl: typing.Optional[int] = None + self, key: str, cache: bool = False, cache_ttl: typing.Optional[int] = None ) -> typing.Optional[typing.Dict]: output: typing.Optional[typing.Dict] = self._backend.execute( - 'get_autologin', + "get_autologin", key=key, cache=cache, cache_ttl=cache_ttl, @@ -138,10 +130,10 @@ def get_session( self, session_id: str, cache: bool = False, - cache_ttl: typing.Optional[int] = None + cache_ttl: typing.Optional[int] = None, ) -> typing.Optional[typing.Dict]: output: typing.Optional[typing.Dict] = self._backend.execute( - 'get_session', + "get_session", session_id=session_id, cache=cache, cache_ttl=cache_ttl, @@ -149,13 +141,10 @@ def get_session( return output def get_user( - self, - user_id: int, - cache: bool = False, - cache_ttl: typing.Optional[int] = None + self, user_id: int, cache: bool = False, cache_ttl: typing.Optional[int] = None ) -> typing.Optional[typing.Dict]: output: typing.Optional[typing.Dict] = self._backend.execute( - 'get_user', + "get_user", user_id=user_id, cache=cache, cache_ttl=cache_ttl, @@ -163,13 +152,10 @@ def get_user( return output def get_user_profile( - self, - user_id: int, - cache: bool = False, - cache_ttl: typing.Optional[int] = None + self, user_id: int, cache: bool = False, cache_ttl: typing.Optional[int] = None ) -> typing.Optional[typing.Dict]: output: typing.Optional[typing.Dict] = self._backend.execute( - 'get_user_profile', + "get_user_profile", user_id=user_id, cache=cache, cache_ttl=cache_ttl, @@ -184,7 +170,7 @@ def has_membership( cache_ttl: typing.Optional[int] = None, ) -> typing.Optional[bool]: output: typing.Optional[bool] = self._backend.execute( - 'has_membership', + "has_membership", user_id=user_id, group_id=group_id, cache=cache, @@ -200,7 +186,7 @@ def has_membership_resolve( cache_ttl: typing.Optional[int] = None, ) -> typing.Optional[bool]: output: typing.Optional[bool] = self._backend.execute( - 'has_membership_resolve', + "has_membership_resolve", user_id=user_id, group_name=group_name, cache=cache, @@ -215,14 +201,13 @@ def fetch_acl_options( cache: bool = False, cache_ttl: typing.Optional[int] = None, ) -> typing.Optional[typing.List[typing.Dict]]: - output: typing.Optional[typing.List[typing.Dict]] =\ - self._backend.execute( - 'fetch_acl_options', - skip=skip, - limit=limit, - cache=cache, - cache_ttl=cache_ttl, - ) + output: typing.Optional[typing.List[typing.Dict]] = self._backend.execute( + "fetch_acl_options", + skip=skip, + limit=limit, + cache=cache, + cache_ttl=cache_ttl, + ) return output def get_unread_notifications_count( @@ -232,7 +217,7 @@ def get_unread_notifications_count( cache_ttl: typing.Optional[int] = None, ) -> typing.Optional[typing.Dict]: output: typing.Optional[typing.Dict] = self._backend.execute( - 'get_unread_notifications_count', + "get_unread_notifications_count", user_id=user_id, cache=cache, cache_ttl=cache_ttl, @@ -240,8 +225,7 @@ def get_unread_notifications_count( return output def get_user_acl( - self, - raw_user_permissions: str + self, raw_user_permissions: str ) -> flask_phpbb3.backends.base.UserAcl: return self._backend.get_user_acl(raw_user_permissions) @@ -250,18 +234,16 @@ def execute_custom( command: str, cache: bool = False, cache_ttl: typing.Optional[int] = None, - **kwargs: typing.Any + **kwargs: typing.Any, ) -> typing.Any: output = self._backend.execute( - command, - cache=cache, - cache_ttl=cache_ttl, - **kwargs + command, cache=cache, cache_ttl=cache_ttl, **kwargs ) return output def teardown(self, exception: typing.Any) -> None: - backend: flask_phpbb3.backends.base.BaseBackend =\ - flask.g.get('phpbb3_backend', None) + backend: flask_phpbb3.backends.base.BaseBackend = flask.g.get( + "phpbb3_backend", None + ) if backend is not None: backend.close() diff --git a/flask_phpbb3/sessions.py b/flask_phpbb3/sessions.py index 00ea94b..849e212 100644 --- a/flask_phpbb3/sessions.py +++ b/flask_phpbb3/sessions.py @@ -44,7 +44,7 @@ def __delitem__(self, key: str) -> None: @property def _phpbb3(self) -> flask_phpbb3.PhpBB3: - output: flask_phpbb3.PhpBB3 = getattr(flask.current_app, 'phpbb3') + output: flask_phpbb3.PhpBB3 = getattr(flask.current_app, "phpbb3") return output def pop(self, *args: typing.Any, **kwargs: typing.Any) -> typing.Any: @@ -59,26 +59,24 @@ def clear(self) -> None: @property def is_authenticated(self) -> bool: """Helper method to test if user is authenticated.""" - user_id = int(self.get('user_id', 1)) + user_id = int(self.get("user_id", 1)) return user_id > 1 def is_member(self, group: typing.Union[int, str]) -> bool: """Tests if user is a member of specified group.""" if isinstance(group, int): # Try with default group - if group == self['group_id']: + if group == self["group_id"]: return True # Access database output = self._phpbb3.has_membership( - user_id=self['user_id'], - group_id=group + user_id=self["user_id"], group_id=group ) else: # Use group name output = self._phpbb3.has_membership_resolve( - user_id=self['user_id'], - group_name=group + user_id=self["user_id"], group_name=group ) if output is None: @@ -88,99 +86,90 @@ def is_member(self, group: typing.Union[int, str]) -> bool: def has_privilege(self, option: str, forum_id: int = 0) -> bool: """Test if user has global or local (if forum_id is set) privileges.""" if not self._acl: - self._acl = self._phpbb3.get_user_acl(self['user_permissions']) + self._acl = self._phpbb3.get_user_acl(self["user_permissions"]) return self._acl.has_privilege(option, forum_id) def has_privileges(self, *options: str, **kwargs: int) -> bool: - forum_id = kwargs.get('forum_id', 0) + forum_id = kwargs.get("forum_id", 0) if not self._acl: - self._acl = self._phpbb3.get_user_acl(self['user_permissions']) + self._acl = self._phpbb3.get_user_acl(self["user_permissions"]) return self._acl.has_privileges(*options, forum_id=forum_id) def get_link_hash(self, link: str) -> str: """Returns link hash.""" if not self.is_authenticated: - return '' + return "" import hashlib - prepared_link: str = self['user_form_salt'] + link - return hashlib.sha1(prepared_link.encode('utf-8')).hexdigest()[:8] + + prepared_link: str = self["user_form_salt"] + link + return hashlib.sha1(prepared_link.encode("utf-8")).hexdigest()[:8] @property def num_unread_notifications(self) -> int: """Returns number of unread notifications.""" - if 'num_unread_notifications' not in self._request_cache: + if "num_unread_notifications" not in self._request_cache: result = self._phpbb3.get_unread_notifications_count( - user_id=self['user_id'] + user_id=self["user_id"] ) if result: - unread_count = int(result['num']) + unread_count = int(result["num"]) else: unread_count = 0 - self._request_cache['num_unread_notifications'] = unread_count + self._request_cache["num_unread_notifications"] = unread_count else: - unread_count = self._request_cache['num_unread_notifications'] + unread_count = self._request_cache["num_unread_notifications"] return unread_count class PhpBB3SessionInterface(flask.sessions.SessionInterface): """A read-only session interface to access phpBB3 session.""" + session_class = PhpBB3Session @classmethod def _cache(cls, app: flask.Flask) -> cachelib.BaseCache: - if not hasattr(app, 'phpbb3_cache'): - raise ValueError( - 'App not properly configured, phpbb3_cache is missing!' - ) - output: cachelib.BaseCache = getattr(app, 'phpbb3_cache') + if not hasattr(app, "phpbb3_cache"): + raise ValueError("App not properly configured, phpbb3_cache is missing!") + output: cachelib.BaseCache = getattr(app, "phpbb3_cache") return output - def _is_bot( - self, - app: flask.Flask, - request: flask.wrappers.Request - ) -> bool: - for user_agent in app.config['PHPBB3_BOTLIST']: - if request.headers.get('User-Agent', '').startswith(user_agent): + def _is_bot(self, app: flask.Flask, request: flask.wrappers.Request) -> bool: + for user_agent in app.config["PHPBB3_BOTLIST"]: + if request.headers.get("User-Agent", "").startswith(user_agent): return True return False def open_session( - self, - app: flask.Flask, - request: flask.wrappers.Request + self, app: flask.Flask, request: flask.wrappers.Request ) -> PhpBB3Session: - cookie_name: str = app.config.get('PHPBB3_COOKIE_NAME', 'phpbb3_') + cookie_name: str = app.config.get("PHPBB3_COOKIE_NAME", "phpbb3_") - if not hasattr(app, 'phpbb3'): - raise ValueError('App not properly configured, phpbb3 is missing!') - phpbb3: flask_phpbb3.PhpBB3 = getattr(app, 'phpbb3') + if not hasattr(app, "phpbb3"): + raise ValueError("App not properly configured, phpbb3 is missing!") + phpbb3: flask_phpbb3.PhpBB3 = getattr(app, "phpbb3") - session_id: typing.Optional[str] = request.args.get('sid', type=str)\ - or request.cookies.get(cookie_name + 'sid', None) + session_id: typing.Optional[str] = request.args.get( + "sid", type=str + ) or request.cookies.get(cookie_name + "sid", None) if not session_id: session_id = None user: typing.Optional[typing.Dict] = None if self._is_bot(app, request): - user = {'user_id': 1, 'username': 'Anonymous'} + user = {"user_id": 1, "username": "Anonymous"} elif session_id: # Try to fetch session user = phpbb3.get_session(session_id=session_id) - if user and 'username' in user: - user['username'] = user['username'] + if user and "username" in user: + user["username"] = user["username"] if not user: # Use anonymous user - user = phpbb3.get_user( - user_id=1, - cache=True, - cache_ttl=ANONYMOUS_CACHE_TTL - ) + user = phpbb3.get_user(user_id=1, cache=True, cache_ttl=ANONYMOUS_CACHE_TTL) # Create session session: PhpBB3Session = self.session_class() @@ -191,11 +180,11 @@ def open_session( session.update(user) # Read from local storage backend - if 'session_id' in session: + if "session_id" in session: cache = self._cache(app) - data = cache.get('sessions_' + session['session_id']) + data = cache.get("sessions_" + session["session_id"]) try: - data = json.loads(data or '') + data = json.loads(data or "") except ValueError: data = None if not isinstance(data, dict): @@ -210,22 +199,26 @@ def save_session( self, app: flask.Flask, session: flask.sessions.SessionMixin, - response: flask.wrappers.Response + response: flask.wrappers.Response, ) -> None: if not isinstance(session, PhpBB3Session): - raise TypeError('The provided session is not PhpBB3Session!') + raise TypeError("The provided session is not PhpBB3Session!") """Currenlty does nothing.""" if session.modified and session._read_only_properties: # Store all 'storable' properties - data = dict([(k, v) - for k, v in session.items() - if k not in session._read_only_properties]) + data = dict( + [ + (k, v) + for k, v in session.items() + if k not in session._read_only_properties + ] + ) - if 'session_id' in session: + if "session_id" in session: # TODO Read session validity from phpbb3 config self._cache(app).set( - 'sessions_' + session['session_id'], + "sessions_" + session["session_id"], json.dumps(data), - timeout=int(3600 * 1.5) + timeout=int(3600 * 1.5), ) diff --git a/tests/integration/base.py b/tests/integration/base.py index bb6a62d..5851fa1 100644 --- a/tests/integration/base.py +++ b/tests/integration/base.py @@ -8,10 +8,10 @@ import psycopg2.extensions -DB_HOST = '127.0.0.1' -DB_ROOT_USER = 'postgres' -DB_USER = 'phpbb3_test' -DB_NAME = 'phpbb3_test' +DB_HOST = "127.0.0.1" +DB_ROOT_USER = "postgres" +DB_USER = "phpbb3_test" +DB_NAME = "phpbb3_test" def setUpModule() -> None: @@ -27,44 +27,42 @@ def tearDownModule() -> None: class TestWithDatabase(unittest.TestCase): def setUp(self) -> None: - self.app = flask.Flask('test_app') - self.app.config.update({ - 'PHPBB3': { - 'DRIVER': 'psycopg2', - 'VERSION': '3.2', - }, - 'PHPBB3_DATABASE': { - 'HOST': DB_HOST, - 'DATABASE': DB_NAME, - 'USER': DB_USER, - 'PASSWORD': '', - 'TABLE_PREFIX': 'phpbb_', - }, - 'SERVER_NAME': '127.0.0.1' - }) + self.app = flask.Flask("test_app") + self.app.config.update( + { + "PHPBB3": { + "DRIVER": "psycopg2", + "VERSION": "3.2", + }, + "PHPBB3_DATABASE": { + "HOST": DB_HOST, + "DATABASE": DB_NAME, + "USER": DB_USER, + "PASSWORD": "", + "TABLE_PREFIX": "phpbb_", + }, + "SERVER_NAME": "127.0.0.1", + } + ) self.phpbb3 = flask_phpbb3.PhpBB3(self.app) # From these lines devil is born - @self.app.route('/') + @self.app.route("/") def index() -> str: return flask.render_template_string( - '{{ session.user_id}},{{ session.username }}' + "{{ session.user_id}},{{ session.username }}" ) - @self.app.route('/data') + @self.app.route("/data") def data() -> str: - return flask.render_template_string( - '{{ session.custom_var}}' - ) + return flask.render_template_string("{{ session.custom_var}}") - @self.app.route('/data/') + @self.app.route("/data/") def set_data(package: str) -> str: - flask.session['custom_var'] = package - return flask.render_template_string( - 'Done :o' - ) + flask.session["custom_var"] = package + return flask.render_template_string("Done :o") - @self.app.route('/priv_test') + @self.app.route("/priv_test") def test_privileges() -> str: return flask.render_template_string( "{{ session.has_privilege('m_edit') }}," @@ -76,8 +74,7 @@ def test_privileges() -> str: self.ctx.push() # Init connection - self.connection: psycopg2.extensions.connection =\ - self.phpbb3._backend._db + self.connection: psycopg2.extensions.connection = self.phpbb3._backend._db self.cursor: psycopg2.extensions.cursor = self.connection.cursor() # Setup client @@ -99,49 +96,45 @@ def _create_user(cursor: psycopg2.extensions.cursor) -> None: def _create_session( - cursor: psycopg2.extensions.cursor, - session_id: str, - user_id: int + cursor: psycopg2.extensions.cursor, session_id: str, user_id: int ) -> None: cursor.execute( "insert into" " phpbb_sessions (session_id, session_user_id)" - " values (%(session_id)s, %(user_id)s)", { - 'session_id': session_id, - 'user_id': user_id, - } + " values (%(session_id)s, %(user_id)s)", + { + "session_id": session_id, + "user_id": user_id, + }, ) def _create_privilege( - cursor: psycopg2.extensions.cursor, - privilege_id: int, - privilege: str + cursor: psycopg2.extensions.cursor, privilege_id: int, privilege: str ) -> None: cursor.execute( "insert into" " phpbb_acl_options (auth_option_id, auth_option, is_global)" - " values (%(privilege_id)s, %(privilege)s, 1)", { - 'privilege_id': privilege_id, - 'privilege': privilege, - } + " values (%(privilege_id)s, %(privilege)s, 1)", + { + "privilege_id": privilege_id, + "privilege": privilege, + }, ) -def _grant_privilege( - cursor: psycopg2.extensions.cursor, - user_id: int -) -> None: +def _grant_privilege(cursor: psycopg2.extensions.cursor, user_id: int) -> None: # Cryptic value to allow only m_edit permission - permission_set = 'HRA0HS' + permission_set = "HRA0HS" cursor.execute( "update phpbb_users" " set" " user_permissions=%(permission_set)s" - " where user_id=%(user_id)s", { - 'user_id': user_id, - 'permission_set': permission_set, - } + " where user_id=%(user_id)s", + { + "user_id": user_id, + "permission_set": permission_set, + }, ) @@ -150,8 +143,9 @@ def _create_db() -> None: connection.set_isolation_level(0) cursor = connection.cursor() - cursor.execute('create user {user}'.format(user=DB_USER)) - cursor.execute('create database {db_name} owner {user};'.format( + cursor.execute("create user {user}".format(user=DB_USER)) + cursor.execute( + "create database {db_name} owner {user};".format( user=DB_USER, db_name=DB_NAME, ) @@ -161,7 +155,7 @@ def _create_db() -> None: def _init_schema(connection: psycopg2.extensions.connection) -> None: - with open('./tests/fixtures/postgres/schema.sql', 'r') as f: + with open("./tests/fixtures/postgres/schema.sql", "r") as f: schema_sql = f.read() cursor_schema = connection.cursor() cursor_schema.execute(schema_sql) @@ -173,26 +167,22 @@ def _destory_db() -> None: connection.set_isolation_level(0) cursor = connection.cursor() - cursor.execute('drop database {db_name};'.format( + cursor.execute( + "drop database {db_name};".format( db_name=DB_NAME, ) ) - cursor.execute('drop user {user}'.format(user=DB_USER)) + cursor.execute("drop user {user}".format(user=DB_USER)) cursor.close() connection.close() def _get_connection( - host: str, - user: str, - database: str + host: str, user: str, database: str ) -> psycopg2.extensions.connection: - connection_string = ( - 'dbname={db_name}' - ' user={user}' - ) + connection_string = "dbname={db_name}" " user={user}" if host: - connection_string += ' host={db_host}' + connection_string += " host={db_host}" connection = psycopg2.connect( connection_string.format( diff --git a/tests/integration/test_extension.py b/tests/integration/test_extension.py index 785daa7..beefaa5 100644 --- a/tests/integration/test_extension.py +++ b/tests/integration/test_extension.py @@ -9,7 +9,7 @@ class TestExtension(base.TestWithDatabase): - @mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend.close') + @mock.patch("flask_phpbb3.backends.psycopg2.Psycopg2Backend.close") def test_teardown(self, mocked_close: mock.Mock) -> None: self.ctx.pop() mocked_close.assert_called() @@ -18,59 +18,79 @@ def test_teardown(self, mocked_close: mock.Mock) -> None: class TestGetUser(base.TestWithDatabase): def test_anonymous_user(self) -> None: - phpbb3: PhpBB3 = getattr(self.app, 'phpbb3') + phpbb3: PhpBB3 = getattr(self.app, "phpbb3") anonymous_user = phpbb3.get_user(user_id=1) self.assertIsNotNone(anonymous_user) if anonymous_user is not None: - self.assertEqual(anonymous_user['username'], 'Anonymous') - self.assertEqual(anonymous_user['user_id'], 1) + self.assertEqual(anonymous_user["username"], "Anonymous") + self.assertEqual(anonymous_user["user_id"], 1) def test_user(self) -> None: base._create_user(self.cursor) - phpbb3: PhpBB3 = getattr(self.app, 'phpbb3') + phpbb3: PhpBB3 = getattr(self.app, "phpbb3") user = phpbb3.get_user(user_id=2) self.assertIsNotNone(user) if user is not None: - self.assertEqual(user['username'], 'test') - self.assertEqual(user['user_id'], 2) + self.assertEqual(user["username"], "test") + self.assertEqual(user["user_id"], 2) def test_unknown_user(self) -> None: - phpbb3: PhpBB3 = getattr(self.app, 'phpbb3') + phpbb3: PhpBB3 = getattr(self.app, "phpbb3") unknown_user = phpbb3.get_user(user_id=2) self.assertEqual(unknown_user, None) class TestFetch(base.TestWithDatabase): def test_paging(self) -> None: - base._create_privilege(self.cursor, 1, 'm_edit') - base._create_privilege(self.cursor, 2, 'm_delete') - base._create_privilege(self.cursor, 3, 'm_some_random') - - expected_privileges = [(0, [{ - 'auth_option': 'm_edit', - 'auth_option_id': 1, - 'founder_only': 0, - 'is_global': 1, - 'is_local': 0, - }]), (1, [{ - 'auth_option': 'm_delete', - 'auth_option_id': 2, - 'founder_only': 0, - 'is_global': 1, - 'is_local': 0, - }]), (2, [{ - 'auth_option': 'm_some_random', - 'auth_option_id': 3, - 'founder_only': 0, - 'is_global': 1, - 'is_local': 0, - }]), (3, [])] + base._create_privilege(self.cursor, 1, "m_edit") + base._create_privilege(self.cursor, 2, "m_delete") + base._create_privilege(self.cursor, 3, "m_some_random") + + expected_privileges = [ + ( + 0, + [ + { + "auth_option": "m_edit", + "auth_option_id": 1, + "founder_only": 0, + "is_global": 1, + "is_local": 0, + } + ], + ), + ( + 1, + [ + { + "auth_option": "m_delete", + "auth_option_id": 2, + "founder_only": 0, + "is_global": 1, + "is_local": 0, + } + ], + ), + ( + 2, + [ + { + "auth_option": "m_some_random", + "auth_option_id": 3, + "founder_only": 0, + "is_global": 1, + "is_local": 0, + } + ], + ), + (3, []), + ] for skip in range(0, 4): - phpbb3: PhpBB3 = getattr(self.app, 'phpbb3') + phpbb3: PhpBB3 = getattr(self.app, "phpbb3") privilege = phpbb3.fetch_acl_options(skip=skip, limit=1) self.assertEqual((skip, privilege), expected_privileges[skip]) @@ -78,81 +98,67 @@ def test_paging(self) -> None: class TestSession(base.TestWithDatabase): def setUp(self) -> None: super(TestSession, self).setUp() - self.session_id = '123' + self.session_id = "123" def test_anonymous(self) -> None: - data = self.client.get('/').get_data().decode('utf-8') - self.assertEqual(data, '1,Anonymous') + data = self.client.get("/").get_data().decode("utf-8") + self.assertEqual(data, "1,Anonymous") def test_invalid_session(self) -> None: base._create_user(self.cursor) - data = self.client.get('/?sid=123').get_data().decode('utf-8') - self.assertEqual(data, '1,Anonymous') + data = self.client.get("/?sid=123").get_data().decode("utf-8") + self.assertEqual(data, "1,Anonymous") def test_user_by_args(self) -> None: base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - data = self.client.get('/?sid=' + self.session_id)\ - .get_data()\ - .decode('utf-8') - self.assertEqual(data, '2,test') + data = self.client.get("/?sid=" + self.session_id).get_data().decode("utf-8") + self.assertEqual(data, "2,test") def test_user_by_cookie(self) -> None: base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - self.client.set_cookie( - 'phpbb3_sid', - self.session_id, - domain='127.0.0.1' - ) - data = self.client.get('/').get_data().decode('utf-8') - self.assertEqual(data, '2,test') - self.client.delete_cookie('phpbb3_sid', domain='127.0.0.1') + self.client.set_cookie("phpbb3_sid", self.session_id, domain="127.0.0.1") + data = self.client.get("/").get_data().decode("utf-8") + self.assertEqual(data, "2,test") + self.client.delete_cookie("phpbb3_sid", domain="127.0.0.1") def test_storage(self) -> None: base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - self.client.set_cookie( - 'phpbb3_sid', - self.session_id, - domain='127.0.0.1' - ) - data = self.client.get('/data').get_data().decode('utf-8') - self.assertEqual(data, '') + self.client.set_cookie("phpbb3_sid", self.session_id, domain="127.0.0.1") + data = self.client.get("/data").get_data().decode("utf-8") + self.assertEqual(data, "") - self.client.get('/data/something') + self.client.get("/data/something") - data = self.client.get('/data').get_data().decode('utf-8') - self.assertEqual(data, 'something') + data = self.client.get("/data").get_data().decode("utf-8") + self.assertEqual(data, "something") def test_storage_invalid_id(self) -> None: - data = self.client.get('/data').get_data().decode('utf-8') - self.assertEqual(data, '') + data = self.client.get("/data").get_data().decode("utf-8") + self.assertEqual(data, "") - self.client.get('/data/something') + self.client.get("/data/something") - data = self.client.get('/data').get_data().decode('utf-8') - self.assertEqual(data, '') + data = self.client.get("/data").get_data().decode("utf-8") + self.assertEqual(data, "") def test_privilege(self) -> None: base._create_user(self.cursor) base._create_session(self.cursor, self.session_id, 2) - base._create_privilege(self.cursor, 1, 'm_edit') + base._create_privilege(self.cursor, 1, "m_edit") base._grant_privilege(self.cursor, 2) - data = self.client.get('/priv_test').get_data().decode('utf-8') - self.assertEqual(data, 'False,False,False') + data = self.client.get("/priv_test").get_data().decode("utf-8") + self.assertEqual(data, "False,False,False") # We do a login via phpbb3 :P - self.client.set_cookie( - 'phpbb3_sid', - self.session_id, - domain='127.0.0.1' - ) - - data = self.client.get('/priv_test').get_data().decode('utf-8') - self.assertEqual(data, 'True,False,True') + self.client.set_cookie("phpbb3_sid", self.session_id, domain="127.0.0.1") + + data = self.client.get("/priv_test").get_data().decode("utf-8") + self.assertEqual(data, "True,False,True") diff --git a/tests/unit/backends/test_base.py b/tests/unit/backends/test_base.py index 633f8c0..5489646 100644 --- a/tests/unit/backends/test_base.py +++ b/tests/unit/backends/test_base.py @@ -7,138 +7,99 @@ class TestSessionHasPrivilege(unittest.TestCase): def setUp(self) -> None: user_acl = { - '0': ['0'] * 31, - '5': ['0'] * 31, + "0": ["0"] * 31, + "5": ["0"] * 31, } - user_acl['0'][0] = '1' - user_acl['0'][3] = '1' - user_acl['5'][3] = '1' + user_acl["0"][0] = "1" + user_acl["0"][3] = "1" + user_acl["5"][3] = "1" - self.user_acl = flask_phpbb3.backends.base.UserAcl([], '') + self.user_acl = flask_phpbb3.backends.base.UserAcl([], "") self.user_acl._acl_options = { - 'local': { - 'm_edit': 0, - 'm_view': 1, - 'm_review': 3, - 'm_strange': 50, + "local": { + "m_edit": 0, + "m_view": 1, + "m_review": 3, + "m_strange": 50, }, - 'global': { - 'm_edit': 0, - 'm_view': 1, - 'm_delete': 3, - 'm_strange': 50, + "global": { + "m_edit": 0, + "m_view": 1, + "m_delete": 3, + "m_strange": 50, }, } self.user_acl._acl = { - '0': ''.join(user_acl['0']), - '5': ''.join(user_acl['5']), + "0": "".join(user_acl["0"]), + "5": "".join(user_acl["5"]), } def test_existing(self) -> None: actual_result = self.user_acl.has_privilege( - 'm_edit', + "m_edit", ) self.assertTrue(actual_result) actual_result = self.user_acl.has_privilege( - 'm_view', + "m_view", ) self.assertFalse(actual_result) def test_global(self) -> None: # m_review is not global, and is false (even tho on global the index # is set to true) - actual_result = self.user_acl.has_privilege( - 'm_review' - ) + actual_result = self.user_acl.has_privilege("m_review") self.assertFalse(actual_result) # m_delete is global, and is True # Do note, both privileges have same index, just on different plains - actual_result = self.user_acl.has_privilege( - 'm_delete' - ) + actual_result = self.user_acl.has_privilege("m_delete") self.assertTrue(actual_result) def test_local(self) -> None: # Now, this is True since we are on local level - actual_result = self.user_acl.has_privilege( - 'm_review', - forum_id=5 - ) + actual_result = self.user_acl.has_privilege("m_review", forum_id=5) self.assertTrue(actual_result) # True, because globals are always set - actual_result = self.user_acl.has_privilege( - 'm_delete', - forum_id=5 - ) + actual_result = self.user_acl.has_privilege("m_delete", forum_id=5) self.assertTrue(actual_result) - actual_result = self.user_acl.has_privilege( - 'm_edit', - forum_id=5 - ) + actual_result = self.user_acl.has_privilege("m_edit", forum_id=5) self.assertTrue(actual_result) - actual_result = self.user_acl.has_privilege( - 'm_view', - forum_id=5 - ) + actual_result = self.user_acl.has_privilege("m_view", forum_id=5) self.assertFalse(actual_result) def test_negated(self) -> None: - actual_result = self.user_acl.has_privilege( - '!m_review', - forum_id=5 - ) + actual_result = self.user_acl.has_privilege("!m_review", forum_id=5) self.assertFalse(actual_result) - actual_result = self.user_acl.has_privilege( - '!m_review' - ) + actual_result = self.user_acl.has_privilege("!m_review") self.assertTrue(actual_result) - actual_result = self.user_acl.has_privilege( - '!m_edit' - ) + actual_result = self.user_acl.has_privilege("!m_edit") self.assertFalse(actual_result) - actual_result = self.user_acl.has_privilege( - '!m_edit', - forum_id=5 - ) + actual_result = self.user_acl.has_privilege("!m_edit", forum_id=5) self.assertFalse(actual_result) def test_out_of_bound(self) -> None: - actual_result = self.user_acl.has_privilege( - 'm_strange' - ) + actual_result = self.user_acl.has_privilege("m_strange") self.assertFalse(actual_result) - actual_result = self.user_acl.has_privilege( - 'm_strange', - forum_id=5 - ) + actual_result = self.user_acl.has_privilege("m_strange", forum_id=5) self.assertFalse(actual_result) def test_unknown(self) -> None: - actual_result = self.user_acl.has_privilege( - 'm_unknown' - ) + actual_result = self.user_acl.has_privilege("m_unknown") self.assertFalse(actual_result) - actual_result = self.user_acl.has_privilege( - 'm_unknown', - forum_id=5 - ) + actual_result = self.user_acl.has_privilege("m_unknown", forum_id=5) self.assertFalse(actual_result) - actual_result = self.user_acl.has_privilege( - 'm_unknown', - forum_id=2 - ) + actual_result = self.user_acl.has_privilege("m_unknown", forum_id=2) self.assertFalse(actual_result) @@ -146,12 +107,12 @@ class TestSessionHasPrivileges(unittest.TestCase): def setUp(self) -> None: self.user_acl = flask_phpbb3.backends.base.UserAcl( raw_acl_options=[], - raw_user_permissions='', + raw_user_permissions="", ) - @mock.patch('flask_phpbb3.backends.base.UserAcl.has_privilege') + @mock.patch("flask_phpbb3.backends.base.UserAcl.has_privilege") def test_combinations(self, has_privilege_mock: mock.Mock) -> None: - privileges = ('m_edit', 'm_delete', 'm_view') + privileges = ("m_edit", "m_delete", "m_view") has_privilege_mock.return_value = False actual_result = self.user_acl.has_privileges(*privileges) @@ -169,22 +130,27 @@ def test_combinations(self, has_privilege_mock: mock.Mock) -> None: actual_result = self.user_acl.has_privileges(*privileges) self.assertTrue(actual_result) - @mock.patch('flask_phpbb3.backends.base.UserAcl.has_privilege', - return_value=False) + @mock.patch("flask_phpbb3.backends.base.UserAcl.has_privilege", return_value=False) def test_per_forum(self, has_privilege_mock: mock.Mock) -> None: - privileges = ('m_edit', 'm_delete', 'm_view') + privileges = ("m_edit", "m_delete", "m_view") self.user_acl.has_privileges(*privileges) - has_privilege_mock.assert_has_calls([ - mock.call('m_edit', forum_id=0), - mock.call('m_delete', forum_id=0), - mock.call('m_view', forum_id=0), - ], any_order=True) + has_privilege_mock.assert_has_calls( + [ + mock.call("m_edit", forum_id=0), + mock.call("m_delete", forum_id=0), + mock.call("m_view", forum_id=0), + ], + any_order=True, + ) has_privilege_mock.reset_mock() self.user_acl.has_privileges(*privileges, forum_id=2) - has_privilege_mock.assert_has_calls([ - mock.call('m_edit', forum_id=2), - mock.call('m_delete', forum_id=2), - mock.call('m_view', forum_id=2), - ], any_order=True) + has_privilege_mock.assert_has_calls( + [ + mock.call("m_edit", forum_id=2), + mock.call("m_delete", forum_id=2), + mock.call("m_view", forum_id=2), + ], + any_order=True, + ) diff --git a/tests/unit/backends/test_psycopg2.py b/tests/unit/backends/test_psycopg2.py index 4e2b382..5c8ad0a 100644 --- a/tests/unit/backends/test_psycopg2.py +++ b/tests/unit/backends/test_psycopg2.py @@ -6,33 +6,28 @@ import flask_phpbb3.backends.psycopg2 -@mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend._db') +@mock.patch("flask_phpbb3.backends.psycopg2.Psycopg2Backend._db") class TestExecuteOperation(unittest.TestCase): def setUp(self) -> None: self.connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { - 'TABLE_PREFIX': '', - } + "TABLE_PREFIX": "", + }, ) def test_get_query(self, mocked_db: mock.Mock) -> None: parameters = mock.Mock() cursor = mock.Mock() - cursor.fetchone.return_value = {'key': 'value'} + cursor.fetchone.return_value = {"key": "value"} mocked_db.cursor.return_value = cursor actual_value = self.connection._execute_operation( - 'get', - 'select * from somewhere', - parameters + "get", "select * from somewhere", parameters ) - self.assertEqual(actual_value, {'key': 'value'}) - cursor.execute.assert_called_once_with( - 'select * from somewhere', - parameters - ) + self.assertEqual(actual_value, {"key": "value"}) + cursor.execute.assert_called_once_with("select * from somewhere", parameters) def test_has_query_negative(self, mocked_db: mock.Mock) -> None: cursor = mock.Mock() @@ -40,9 +35,7 @@ def test_has_query_negative(self, mocked_db: mock.Mock) -> None: mocked_db.cursor.return_value = cursor actual_value = self.connection._execute_operation( - 'has', - 'select * from somewhere', - {} + "has", "select * from somewhere", {} ) self.assertFalse(actual_value) @@ -53,23 +46,19 @@ def test_has_query_positive(self, mocked_db: mock.Mock) -> None: mocked_db.cursor.return_value = cursor actual_value = self.connection._execute_operation( - 'has', - 'select * from somewhere', - {} + "has", "select * from somewhere", {} ) self.assertTrue(actual_value) def test_fetch_query(self, mocked_db: mock.Mock) -> None: - expected_value = [{'key': 1}, {'key': 2}] + expected_value = [{"key": 1}, {"key": 2}] cursor = mock.Mock() cursor.__iter__ = mock.Mock(return_value=iter(expected_value)) mocked_db.cursor.return_value = cursor actual_value = self.connection._execute_operation( - 'fetch', - 'select * from somewhere', - {} + "fetch", "select * from somewhere", {} ) self.assertEqual(actual_value, expected_value) @@ -79,9 +68,7 @@ def test_set_query(self, mocked_db: mock.Mock) -> None: mocked_db.cursor.return_value = cursor actual_value = self.connection._execute_operation( - 'set', - 'select * from somewhere', - {} + "set", "select * from somewhere", {} ) self.assertEqual(actual_value, cursor.statusmessage) @@ -89,146 +76,154 @@ def test_set_query(self, mocked_db: mock.Mock) -> None: def test_unknown_query(self, mocked_db: mock.Mock) -> None: actual_value = self.connection._execute_operation( - 'unknown_op', - 'select * from somewhere', - {} + "unknown_op", "select * from somewhere", {} ) # Internal function, this should never happen self.assertIsNone(actual_value) -@mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend._db') +@mock.patch("flask_phpbb3.backends.psycopg2.Psycopg2Backend._db") class TestPreparedCustomFieldsStatements(unittest.TestCase): def test_empty(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { - 'TABLE_PREFIX': '', - 'CUSTOM_USER_FIELDS': [], - } + "TABLE_PREFIX": "", + "CUSTOM_USER_FIELDS": [], + }, ) self.assertSetEqual( set(connection._functions.keys()), - set([ - 'has_membership_resolve', - 'get_autologin', - 'get_session', - 'has_membership', - 'fetch_acl_options', - 'get_unread_notifications_count', - 'get_user', - 'get_user_profile', - ]) + set( + [ + "has_membership_resolve", + "get_autologin", + "get_session", + "has_membership", + "fetch_acl_options", + "get_unread_notifications_count", + "get_user", + "get_user_profile", + ] + ), ) def test_valid(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { - 'TABLE_PREFIX': '', - 'CUSTOM_USER_FIELDS': ['some_field', 'another_field'], - } + "TABLE_PREFIX": "", + "CUSTOM_USER_FIELDS": ["some_field", "another_field"], + }, ) self.assertSetEqual( set(connection._functions.keys()), - set([ - 'has_membership_resolve', - 'get_autologin', - 'get_session', - 'set_some_field', - 'has_membership', - 'fetch_acl_options', - 'set_another_field', - 'get_unread_notifications_count', - 'get_user', - 'get_user_profile', - ]), - ) - - -@mock.patch('flask_phpbb3.backends.psycopg2.Psycopg2Backend._db') + set( + [ + "has_membership_resolve", + "get_autologin", + "get_session", + "set_some_field", + "has_membership", + "fetch_acl_options", + "set_another_field", + "get_unread_notifications_count", + "get_user", + "get_user_profile", + ] + ), + ) + + +@mock.patch("flask_phpbb3.backends.psycopg2.Psycopg2Backend._db") class TestPreparedCustomStatements(unittest.TestCase): def test_empty(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { - 'TABLE_PREFIX': '', - 'CUSTOM_STATEMENTS': {}, - } + "TABLE_PREFIX": "", + "CUSTOM_STATEMENTS": {}, + }, ) self.assertSetEqual( set(connection._functions.keys()), - set([ - 'has_membership_resolve', - 'get_autologin', - 'get_session', - 'has_membership', - 'fetch_acl_options', - 'get_unread_notifications_count', - 'get_user', - 'get_user_profile', - ]) + set( + [ + "has_membership_resolve", + "get_autologin", + "get_session", + "has_membership", + "fetch_acl_options", + "get_unread_notifications_count", + "get_user", + "get_user_profile", + ] + ), ) def test_addition(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { - 'TABLE_PREFIX': '', - 'CUSTOM_STATEMENTS': { - 'some_custom_statement': 'some query', + "TABLE_PREFIX": "", + "CUSTOM_STATEMENTS": { + "some_custom_statement": "some query", }, - } + }, ) self.assertSetEqual( set(connection._functions.keys()), - set([ - 'has_membership_resolve', - 'get_autologin', - 'get_session', - 'has_membership', - 'fetch_acl_options', - 'get_unread_notifications_count', - 'some_custom_statement', - 'get_user', - 'get_user_profile', - ]) + set( + [ + "has_membership_resolve", + "get_autologin", + "get_session", + "has_membership", + "fetch_acl_options", + "get_unread_notifications_count", + "some_custom_statement", + "get_user", + "get_user_profile", + ] + ), ) self.assertEqual( - connection._functions['some_custom_statement'], - 'some query', + connection._functions["some_custom_statement"], + "some query", ) def test_override(self, mocked_db: mock.Mock) -> None: connection = flask_phpbb3.backends.psycopg2.Psycopg2Backend( cachelib.SimpleCache(), { - 'TABLE_PREFIX': '', - 'CUSTOM_STATEMENTS': { - 'get_autologin': 'overriden', + "TABLE_PREFIX": "", + "CUSTOM_STATEMENTS": { + "get_autologin": "overriden", }, - } + }, ) self.assertSetEqual( set(connection._functions.keys()), - set([ - 'has_membership_resolve', - 'get_autologin', - 'get_session', - 'has_membership', - 'fetch_acl_options', - 'get_unread_notifications_count', - 'get_user', - 'get_user_profile', - ]) + set( + [ + "has_membership_resolve", + "get_autologin", + "get_session", + "has_membership", + "fetch_acl_options", + "get_unread_notifications_count", + "get_user", + "get_user_profile", + ] + ), ) self.assertEqual( - connection._functions['get_autologin'], - 'overriden', + connection._functions["get_autologin"], + "overriden", ) diff --git a/tests/unit/test_sessions.py b/tests/unit/test_sessions.py index d275594..7d6ecfb 100644 --- a/tests/unit/test_sessions.py +++ b/tests/unit/test_sessions.py @@ -14,46 +14,46 @@ class TestSessionMutability(TestSession): def test_main(self) -> None: self.assertFalse(self.session.modified) - self.session['a_key'] = 'some_value' + self.session["a_key"] = "some_value" self.assertTrue(self.session.modified) def test_double_set(self) -> None: self.assertFalse(self.session.modified) - self.session['a_key'] = 'some_value' + self.session["a_key"] = "some_value" - self.session['a_key'] = 'some_value' + self.session["a_key"] = "some_value" self.assertTrue(self.session.modified) def test_same_value(self) -> None: self.assertFalse(self.session.modified) - self.session['a_key'] = None # type: ignore + self.session["a_key"] = None # type: ignore self.assertFalse(self.session.modified) def test_deletion(self) -> None: - self.session['a_key'] = 'some_value' + self.session["a_key"] = "some_value" self.session.modified = False self.assertFalse(self.session.modified) - del self.session['a_key'] + del self.session["a_key"] self.assertTrue(self.session.modified) def test_pop(self) -> None: - self.session['a_key'] = 'some_value' + self.session["a_key"] = "some_value" self.session.modified = False self.assertFalse(self.session.modified) - actual_result = self.session.pop('a_key') - self.assertEqual(actual_result, 'some_value') + actual_result = self.session.pop("a_key") + self.assertEqual(actual_result, "some_value") self.assertTrue(self.session.modified) def test_read_only(self) -> None: - self.session._read_only_properties.add('a_key') + self.session._read_only_properties.add("a_key") self.assertFalse(self.session.modified) - self.session['a_key'] = 'some_value' + self.session["a_key"] = "some_value" self.assertFalse(self.session.modified) def test_clear(self) -> None: @@ -65,30 +65,28 @@ def test_clear(self) -> None: class TestSessionUser(TestSession): def test_authenticated(self) -> None: - self.session['user_id'] = '1' + self.session["user_id"] = "1" self.assertFalse(self.session.is_authenticated) - self.session['user_id'] = '2' + self.session["user_id"] = "2" self.assertTrue(self.session.is_authenticated) - del self.session['user_id'] + del self.session["user_id"] self.assertFalse(self.session.is_authenticated) - self.session['user_id'] = '-1' + self.session["user_id"] = "-1" self.assertFalse(self.session.is_authenticated) def test_get_link_hash(self) -> None: - some_link = '/my/link' - self.session['user_form_salt'] = 'some_salt' + some_link = "/my/link" + self.session["user_form_salt"] = "some_salt" - self.session['user_id'] = '1' - self.assertEqual(self.session.get_link_hash(some_link), '') + self.session["user_id"] = "1" + self.assertEqual(self.session.get_link_hash(some_link), "") - self.session['user_id'] = '3' - salted_link: str = self.session['user_form_salt'] + some_link - expected_value = hashlib.sha1( - salted_link.encode('utf-8') - ).hexdigest()[:8] + self.session["user_id"] = "3" + salted_link: str = self.session["user_form_salt"] + some_link + expected_value = hashlib.sha1(salted_link.encode("utf-8")).hexdigest()[:8] self.assertEqual(self.session.get_link_hash(some_link), expected_value) @@ -98,13 +96,13 @@ def setUp(self) -> None: self.user_id = 2 self.group_id = 2 - self.session['user_id'] = self.user_id - self.session['group_id'] = 2 + self.session["user_id"] = self.user_id + self.session["group_id"] = 2 def test_default_group_id(self) -> None: self.session.is_member(self.group_id) - @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') + @mock.patch("flask_phpbb3.sessions.PhpBB3Session._phpbb3") def test_group_id(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership.return_value = True @@ -115,7 +113,7 @@ def test_group_id(self, patched_phpbb3: mock.Mock) -> None: group_id=5, ) - @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') + @mock.patch("flask_phpbb3.sessions.PhpBB3Session._phpbb3") def test_group_id_failed(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership.return_value = False @@ -126,29 +124,29 @@ def test_group_id_failed(self, patched_phpbb3: mock.Mock) -> None: group_id=5, ) - @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') + @mock.patch("flask_phpbb3.sessions.PhpBB3Session._phpbb3") def test_group_name(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership_resolve.return_value = True - actual_result = self.session.is_member('group') + actual_result = self.session.is_member("group") self.assertTrue(actual_result) patched_phpbb3.has_membership_resolve.assert_called_once_with( user_id=self.user_id, - group_name='group', + group_name="group", ) - @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') + @mock.patch("flask_phpbb3.sessions.PhpBB3Session._phpbb3") def test_group_name_failed(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership_resolve.return_value = False - actual_result = self.session.is_member('group') + actual_result = self.session.is_member("group") self.assertFalse(actual_result) patched_phpbb3.has_membership_resolve.assert_called_once_with( user_id=self.user_id, - group_name='group', + group_name="group", ) - @mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') + @mock.patch("flask_phpbb3.sessions.PhpBB3Session._phpbb3") def test_nones(self, patched_phpbb3: mock.Mock) -> None: patched_phpbb3.has_membership_resolve.return_value = None patched_phpbb3.has_membership.return_value = None @@ -156,66 +154,66 @@ def test_nones(self, patched_phpbb3: mock.Mock) -> None: actual_result = self.session.is_member(5) self.assertFalse(actual_result) - actual_result = self.session.is_member('group') + actual_result = self.session.is_member("group") self.assertFalse(actual_result) -@mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') +@mock.patch("flask_phpbb3.sessions.PhpBB3Session._phpbb3") class TestSessionUserPrivileges(TestSession): def setUp(self) -> None: super(TestSessionUserPrivileges, self).setUp() - self.session['user_permissions'] = '' + self.session["user_permissions"] = "" def test_load_data_privilege(self, mocked_phpbb3: mock.Mock) -> None: - self.session.has_privilege('m_view') + self.session.has_privilege("m_view") - mocked_phpbb3.get_user_acl.assert_called_once_with('') + mocked_phpbb3.get_user_acl.assert_called_once_with("") def test_load_data_privileges(self, mocked_phpbb3: mock.Mock) -> None: - self.session.has_privileges('m_view', 'm_edit') + self.session.has_privileges("m_view", "m_edit") - mocked_phpbb3.get_user_acl.assert_called_once_with('') + mocked_phpbb3.get_user_acl.assert_called_once_with("") def test_load_data_single(self, mocked_phpbb3: mock.Mock) -> None: - self.session.has_privilege('m_view') - self.session.has_privileges('m_view', 'm_edit') + self.session.has_privilege("m_view") + self.session.has_privileges("m_view", "m_edit") - mocked_phpbb3.get_user_acl.assert_called_once_with('') + mocked_phpbb3.get_user_acl.assert_called_once_with("") def test_call_privilege(self, mocked_phpbb3: mock.Mock) -> None: user_acl = mock.Mock() mocked_phpbb3.get_user_acl.return_value = user_acl forum_id = mock.Mock() - self.session.has_privilege('m_view', forum_id) + self.session.has_privilege("m_view", forum_id) - user_acl.has_privilege.assert_called_once_with('m_view', forum_id) + user_acl.has_privilege.assert_called_once_with("m_view", forum_id) def test_call_privileges(self, mocked_phpbb3: mock.Mock) -> None: user_acl = mock.Mock() mocked_phpbb3.get_user_acl.return_value = user_acl forum_id = mock.Mock() - self.session.has_privileges('m_view', 'm_edit', forum_id=forum_id) + self.session.has_privileges("m_view", "m_edit", forum_id=forum_id) user_acl.has_privileges.assert_called_once_with( - 'm_view', - 'm_edit', + "m_view", + "m_edit", forum_id=forum_id, ) -@mock.patch('flask_phpbb3.sessions.PhpBB3Session._phpbb3') +@mock.patch("flask_phpbb3.sessions.PhpBB3Session._phpbb3") class TestUnreadNotificationsNum(TestSession): def setUp(self) -> None: super(TestUnreadNotificationsNum, self).setUp() - self.session['user_id'] = 2 + self.session["user_id"] = 2 def test_main(self, mocked_phpbb3: mock.Mock) -> None: mocked_phpbb3.get_unread_notifications_count.return_value = { - 'num': 3, + "num": 3, } actual_result = self.session.num_unread_notifications @@ -229,10 +227,10 @@ def test_none(self, mocked_phpbb3: mock.Mock) -> None: def test_cache(self, mocked_phpbb3: mock.Mock) -> None: mocked_phpbb3.get_unread_notifications_count.return_value = { - 'num': 3, + "num": 3, } user_id = mock.Mock() - self.session['user_id'] = user_id + self.session["user_id"] = user_id _ = self.session.num_unread_notifications _ = self.session.num_unread_notifications