diff --git a/.dockerignore b/.dockerignore index 4bca540..cdf1b5f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,6 @@ .docker .git/ +/.venv/ /venv/ /.idea/ **/__pycache__/ diff --git a/.gitignore b/.gitignore index 8fb63e5..2603dc3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ +.venv/* venv/* .idea/* __pycache__ *.egg-info/* .coverage .pytest_cache/ +build/ diff --git a/.tekton/github-mirror-master-pull-request.yaml b/.tekton/github-mirror-master-pull-request.yaml index c3985d5..18dc659 100644 --- a/.tekton/github-mirror-master-pull-request.yaml +++ b/.tekton/github-mirror-master-pull-request.yaml @@ -30,6 +30,8 @@ spec: value: Dockerfile - name: path-context value: . + - name: target-stage + value: test pipelineRef: resolver: git params: diff --git a/.tekton/github-mirror-master-push.yaml b/.tekton/github-mirror-master-push.yaml index ee48009..f27e5bc 100644 --- a/.tekton/github-mirror-master-push.yaml +++ b/.tekton/github-mirror-master-push.yaml @@ -27,6 +27,8 @@ spec: value: Dockerfile - name: path-context value: . + - name: target-stage + value: prod pipelineRef: resolver: git params: diff --git a/Dockerfile b/Dockerfile index aa75170..116a0c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,33 @@ -FROM registry.access.redhat.com/ubi9/python-311:1-77@sha256:3231676407c7e727cfbd853137cfaab2f891410762268fbebe4ea9f27d8f568b as builder +FROM registry.access.redhat.com/ubi9/python-311:1-77@sha256:3231676407c7e727cfbd853137cfaab2f891410762268fbebe4ea9f27d8f568b AS builder +COPY --from=ghcr.io/astral-sh/uv:0.5.5@sha256:dc60491f42c9c7228fe2463f551af49a619ebcc9cbd10a470ced7ada63aa25d4 /uv /bin/uv WORKDIR /ghmirror -RUN python3 -m venv venv -ENV VIRTUAL_ENV=/ghmirror/venv -ENV PATH="$VIRTUAL_ENV/bin:$PATH" -COPY --chown=1001:0 setup.py VERSION ./ -RUN pip install . - -FROM builder as test -COPY --chown=1001:0 requirements-check.txt ./ -RUN pip install -r requirements-check.txt -COPY --chown=1001:0 . ./ -ENTRYPOINT ["make"] -CMD ["check"] +COPY --chown=1001:0 pyproject.toml uv.lock ./ +RUN uv lock --locked +COPY --chown=1001:0 ghmirror ./ghmirror +RUN uv sync --frozen --no-cache --compile-bytecode --no-group dev --python /usr/bin/python3.11 -FROM registry.access.redhat.com/ubi9/ubi-minimal:9.4-1227@sha256:f182b500ff167918ca1010595311cf162464f3aa1cab755383d38be61b4d30aa +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.4-1227@sha256:f182b500ff167918ca1010595311cf162464f3aa1cab755383d38be61b4d30aa AS prod RUN microdnf upgrade -y && \ microdnf install -y python3.11 && \ microdnf clean all COPY LICENSE /licenses/LICENSE -USER 1001 WORKDIR /ghmirror -ENV VIRTUAL_ENV=/ghmirror/venv +RUN chown -R 1001:0 /ghmirror +USER 1001 +ENV VIRTUAL_ENV=/ghmirror/.venv ENV PATH="$VIRTUAL_ENV/bin:$PATH" -COPY --from=builder $VIRTUAL_ENV $VIRTUAL_ENV -COPY --chown=1001:0 . ./ +COPY --from=builder /ghmirror /ghmirror ENTRYPOINT ["gunicorn", "ghmirror.app:APP"] CMD ["--workers", "1", "--threads", "8", "--bind", "0.0.0.0:8080"] + +FROM prod AS test +COPY --from=ghcr.io/astral-sh/uv:0.5.5@sha256:dc60491f42c9c7228fe2463f551af49a619ebcc9cbd10a470ced7ada63aa25d4 /uv /bin/uv +USER root +RUN microdnf install -y make +USER 1001 +COPY --chown=1001:0 Makefile ./ +COPY --chown=1001:0 tests ./tests +ENV UV_NO_CACHE=true +RUN uv sync --frozen +RUN make check + diff --git a/Makefile b/Makefile index 9f85384..522df12 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,11 @@ -develop: - pip install --editable . - pip install -r requirements-check.txt - - check: - ruff check --no-fix - ruff format --check - python3 -m pytest -v --forked --cov=ghmirror --cov-report=term-missing tests/ + uv run ruff check --no-fix + uv run ruff format --check + uv run pytest -v --forked --cov=ghmirror --cov-report=term-missing tests/ accept: python3 acceptance/test_basic.py format: - ruff check - ruff format + uv run ruff check + uv run ruff format diff --git a/VERSION b/VERSION deleted file mode 100644 index 6e8bf73..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.1.0 diff --git a/acceptance/__init__.py b/acceptance/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/acceptance/test_basic.py b/acceptance/test_basic.py index e183081..f1aeed5 100644 --- a/acceptance/test_basic.py +++ b/acceptance/test_basic.py @@ -12,7 +12,7 @@ def test_get_repo(path, code, cache): url = f"{GITHUB_MIRROR_URL}{path}" headers = {"Authorization": f"token {CLIENT_TOKEN}"} - response = requests.get(url, headers=headers) + response = requests.get(url, headers=headers, timeout=60) assert response.status_code == code if cache is not None: diff --git a/ghmirror/app/__init__.py b/ghmirror/app/__init__.py index d8f9e35..ae8398e 100644 --- a/ghmirror/app/__init__.py +++ b/ghmirror/app/__init__.py @@ -12,9 +12,7 @@ # Copyright: Red Hat Inc. 2020 # Author: Amador Pahim -""" -The GitHub Mirror endpoints -""" +"""The GitHub Mirror endpoints""" import logging import os @@ -37,12 +35,10 @@ def error_handler(exception): - """ - Used when an exception happens in the flask app. - """ + """Used when an exception happens in the flask app.""" return ( flask.jsonify( - message=f"Error reaching {GH_API}: {str(exception.__class__.__name__)}" + message=f"Error reaching {GH_API}: {exception.__class__.__name__!s}" ), 502, ) @@ -54,17 +50,13 @@ def error_handler(exception): @APP.route("/healthz", methods=["GET"]) def healthz(): - """ - Health check endpoint for Kubernetes. - """ + """Health check endpoint for Kubernetes.""" return flask.Response("OK") @APP.route("/metrics", methods=["GET"]) def metrics(): - """ - Prometheus metrics endpoint. - """ + """Prometheus metrics endpoint.""" headers = {"Content-type": "text/plain"} stats_cache = StatsCache() @@ -80,9 +72,7 @@ def metrics(): @APP.route("/", methods=["GET", "POST", "PUT", "PATCH", "DELETE"]) @check_user def ghmirror(path): - """ - Default endpoint, matching any url without a specific endpoint. - """ + """Default endpoint, matching any url without a specific endpoint.""" url = f"{GH_API}/{path}" if flask.request.args: @@ -111,4 +101,8 @@ def ghmirror(path): if __name__ == "__main__": # pragma: no cover - APP.run(host="127.0.0.1", debug=True, port="8080") + APP.run( + host="127.0.0.1", + debug=bool(os.environ.get("GITHUB_MIRROR_DEBUG", "1")), + port=8080, + ) diff --git a/ghmirror/core/constants.py b/ghmirror/core/constants.py index 8504df7..1624c2f 100644 --- a/ghmirror/core/constants.py +++ b/ghmirror/core/constants.py @@ -12,9 +12,7 @@ # Copyright: Red Hat Inc. 2020 # Author: Amador Pahim -""" -System constants. -""" +"""System constants.""" GH_API = "https://api.github.com" GH_STATUS_API = "https://www.githubstatus.com/api/v2/components.json" diff --git a/ghmirror/core/mirror_requests.py b/ghmirror/core/mirror_requests.py index a5edfc6..997c774 100644 --- a/ghmirror/core/mirror_requests.py +++ b/ghmirror/core/mirror_requests.py @@ -12,9 +12,7 @@ # Copyright: Red Hat Inc. 2020 # Author: Amador Pahim -""" -Implements conditional requests -""" +"""Implements conditional requests""" # ruff: noqa: PLR2004 import hashlib @@ -35,10 +33,7 @@ def _get_elements_per_page(url_params): - """ - Get 'per_page' parameter if present in URL or - return None if not present - """ + """Get 'per_page' parameter if present in URL or return None if not present""" if url_params is not None: per_page = url_params.get("per_page") if per_page is not None: @@ -48,10 +43,10 @@ def _get_elements_per_page(url_params): def _cache_response(resp, cache, cache_key): - """ - Implements the logic to decide whether or not - whe should cache a request acording to the headers - and content + """Cache response if it makes sense + + Implements the logic to decide whether or not whe should cache a request acording + to the headers and content """ # Caching only makes sense when at least one # of those headers is present @@ -65,10 +60,7 @@ def _cache_response(resp, cache, cache_key): def _online_request( session, method, url, cached_response, headers=None, parameters=None ): - """ - Handle API errors on conditional requests and try - to serve contents from cache - """ + """Handle API errors on conditional requests and try to serve contents from cache""" try: resp = session.request( method=method, @@ -146,9 +138,9 @@ def _handle_not_changed( @requests_metrics def conditional_request(session, method, url, auth, data=None, url_params=None): - """ - Implements conditional requests, checking first whether - the upstream API is online of offline to decide which + """Implements conditional requests. + + Checking first whether the upstream API is online of offline to decide which request routine to call. """ if GithubStatus().online: @@ -156,11 +148,8 @@ def conditional_request(session, method, url, auth, data=None, url_params=None): return offline_request(method, url, auth) -# pylint: disable-msg=too-many-locals def online_request(session, method, url, auth, data=None, url_params=None): - """ - Implements conditional requests. - """ + """Implements conditional requests.""" cache = RequestsCache() headers = {} parameters = url_params.to_dict() if url_params is not None else {} @@ -241,8 +230,7 @@ def online_request(session, method, url, auth, data=None, url_params=None): def _should_error_response_be_served_from_cache(response): - """Parse a response to check if we should serve contents - from cache + """Parse a response to check if we should serve contents from cache :param response: requests module response :type response: requests.Response @@ -251,7 +239,6 @@ def _should_error_response_be_served_from_cache(response): from cache :rtype: str, optional """ - if _is_rate_limit_error(response): return "RATE_LIMITED" @@ -280,9 +267,7 @@ def _is_rate_limit_error(response): def offline_request( method, url, auth, error_code=504, error_message=b'{"message": "gateway timeout"}\n' ): - """ - Implements offline requests (serves content from cache, when possible). - """ + """Implements offline requests (serves content from cache, when possible).""" headers = {} if auth is None: auth_sha = None @@ -299,8 +284,7 @@ def offline_request( response = requests.models.Response() response.status_code = error_code response.headers["X-Cache"] = "OFFLINE_MISS" - # pylint: disable=protected-access - response._content = error_message + response._content = error_message # noqa: SLF001 return response cache = RequestsCache() @@ -320,6 +304,5 @@ def offline_request( response = requests.models.Response() response.status_code = error_code response.headers["X-Cache"] = "OFFLINE_MISS" - # pylint: disable=protected-access - response._content = error_message + response._content = error_message # noqa: SLF001 return response diff --git a/ghmirror/core/mirror_response.py b/ghmirror/core/mirror_response.py index 374fbe8..28e94e3 100644 --- a/ghmirror/core/mirror_response.py +++ b/ghmirror/core/mirror_response.py @@ -12,16 +12,14 @@ # Copyright: Red Hat Inc. 2020 # Author: Amador Pahim -""" -Module containing all the abstractions around an HTTP response. -""" +"""Module containing all the abstractions around an HTTP response.""" class MirrorResponse: - """ - Wrapper around the requests.Response, implementing properties - that replace the strings containing the GutHub API url by the - mirror url where needed. + """Wrapper around the requests.Response. + + Implementing properties that replace the strings containing the + GutHub API url by the mirror url where needed. :param original_response: the return from the original request to the GitHub API @@ -40,10 +38,10 @@ def __init__(self, original_response, gh_api_url, gh_mirror_url): @property def headers(self): - """ - Retrieves the headers we are interested in from the original - response and sanitizes them so we can impersonate the GitHub - API. + """Sanitize headers. + + Retrieves the headers we are interested in from the original response and + sanitizes them so we can impersonate the GitHub API. :return: the sanitized headers :rtype: dict @@ -76,7 +74,8 @@ def headers(self): @property def content(self): - """ + """Sanitize content. + Retrieves the content from the original response and sanitizes them so we can impersonate the GitHub API. @@ -86,17 +85,13 @@ def content(self): if self._original_response.content is None: return None - sanitized_content = self._original_response.content.replace( + return self._original_response.content.replace( self._gh_api_url.encode(), self._gh_mirror_url.encode() ) - return sanitized_content - @property def status_code(self): - """ - Convenience method to expose the original response HTTP - status code. + """Convenience method to expose the original response HTTP status code. :return: the response status code """ diff --git a/ghmirror/data_structures/monostate.py b/ghmirror/data_structures/monostate.py index f74438f..41e5095 100644 --- a/ghmirror/data_structures/monostate.py +++ b/ghmirror/data_structures/monostate.py @@ -12,9 +12,7 @@ # Copyright: Red Hat Inc. 2020 # Author: Amador Pahim -""" -Caching data structures. -""" +"""Caching data structures.""" import hashlib import logging @@ -59,8 +57,8 @@ def __init__(self, sleep_time, timeout, session): self._start_check() def _start_check(self): - """ - Starting a daemon thread to check the GitHub API status. + """Starting a daemon thread to check the GitHub API status. + daemon is required so the thread is killed when the main thread completes. This is also useful for the tests. """ @@ -69,8 +67,8 @@ def _start_check(self): @staticmethod def _is_github_online(response): - """ - Check if the Github API is online based on the response. + """Check if the Github API is online based on the response. + If API Requests component status is major_outage, then it's offline. If API Requests component status is one of operational, degraded_performance, or partial_outage, then it's online. @@ -83,9 +81,7 @@ def _is_github_online(response): @classmethod def create(cls): - """ - Class method to create a new instance of _GithubStatus. - """ + """Class method to create a new instance of _GithubStatus.""" sleep_time = int(os.environ.get("GITHUB_STATUS_SLEEP_TIME", STATUS_SLEEP_TIME)) timeout = int(os.environ.get("GITHUB_STATUS_TIMEOUT", STATUS_TIMEOUT)) session = requests.Session() @@ -93,9 +89,9 @@ def create(cls): return cls(sleep_time=sleep_time, timeout=timeout, session=session) def check(self): - """ - Method to be called in a thread. It will check the - Github API status every self.sleep_time seconds and set + """Method to be called in a thread. + + It will check the Github API status every self.sleep_time seconds and set the self.online accordingly. """ while True: @@ -116,15 +112,13 @@ def check(self): class GithubStatus: - """ - Monostate class for sharing the Github API Status. - """ + """Monostate class for sharing the Github API Status.""" _instance = None _lock = threading.Lock() @classmethod - def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument + def __new__(cls, *args, **kwargs): # noqa: ARG003 with cls._lock: if cls._instance is None: cls._instance = _GithubStatus.create() @@ -132,9 +126,7 @@ def __new__(cls, *args, **kwargs): # pylint: disable=unused-argument class InMemoryCacheBorg: - """ - Monostate class for sharing the in-memory requests cache. - """ + """Monostate class for sharing the in-memory requests cache.""" _state = {} @@ -143,14 +135,12 @@ def __init__(self): class InMemoryCache(InMemoryCacheBorg): - """ - Dictionary-like implementation for caching requests. - """ + """Dictionary-like implementation for caching requests.""" def __getattr__(self, item): - """ - Safe class argument initialization. We do it here - (instead of in the __init__()) so we don't overwrite + """Safe class argument initialization. + + We do it here (instead of in the __init__()) so we don't overwrite them on when a new instance is created. """ setattr(self, item, {}) @@ -183,9 +173,7 @@ def __sizeof__(self): class UsersCacheBorg: - """ - Monostate class for sharing the users cache. - """ + """Monostate class for sharing the users cache.""" _state = {} @@ -194,14 +182,12 @@ def __init__(self): class UsersCache(UsersCacheBorg): - """ - Dict-like implementation for caching users information. - """ + """Dict-like implementation for caching users information.""" def __getattr__(self, item): - """ - Safe class argument initialization. We do it here - (instead of in the __init__()) so we don't overwrite + """Safe class argument initialization. + + We do it here (instead of in the __init__()) so we don't overwrite them when a new instance is created. """ setattr(self, item, {}) @@ -215,22 +201,16 @@ def __contains__(self, item): return self._sha(item) in self._data def add(self, key, value=None): - """ - Adding the value to the backing dict - """ + """Adding the value to the backing dict""" self._data[self._sha(key)] = value def get(self, key): - """ - Getting the value from the backing dict - """ + """Getting the value from the backing dict""" return self._data.get(self._sha(key)) class StatsCacheBorg: - """ - Monostate class for sharing the Statistics. - """ + """Monostate class for sharing the Statistics.""" _state = {} @@ -239,15 +219,13 @@ def __init__(self): class StatsCache(StatsCacheBorg): - """ - Statistics cacher. - """ + """Statistics cacher.""" def __getattr__(self, item): - """ - Safe class argument initialization. We do it here - (instead of in the __init__()) so we don't overwrite - them on when a new instance is created. + """Safe class argument initialization. + + We do it here (instead of in the __init__()) so we don't overwrite + them when a new instance is created. """ if item == "registry": # This will create the self.registry attribute, which @@ -329,27 +307,19 @@ def __getattr__(self, item): return getattr(self, item) def count(self): - """ - Convenience method to increment the counter. - """ + """Convenience method to increment the counter.""" self.counter.inc(1) def observe(self, cache, status, value, method, user): - """ - Convenience method to populate the histogram. - """ + """Convenience method to populate the histogram.""" self.histogram.labels( cache=cache, status=status, method=method, user=user ).observe(value) def set_cache_size(self, value): - """ - Convenience method to set the Gauge. - """ + """Convenience method to set the Gauge.""" self.gauge_cache_size.set(value) def set_cached_objects(self, value): - """ - Convenience method to set the Gauge. - """ + """Convenience method to set the Gauge.""" self.gauge_cached_objects.set(value) diff --git a/ghmirror/data_structures/redis_data_structures.py b/ghmirror/data_structures/redis_data_structures.py index 0f79116..d8fff94 100644 --- a/ghmirror/data_structures/redis_data_structures.py +++ b/ghmirror/data_structures/redis_data_structures.py @@ -12,9 +12,7 @@ # Copyright: Red Hat Inc. 2020 # Author: Maha Ashour -""" -Caching data in Redis. -""" +"""Caching data in Redis.""" import os import pickle @@ -24,15 +22,13 @@ PRIMARY_ENDPOINT = os.environ.get("PRIMARY_ENDPOINT", "localhost") READER_ENDPOINT = os.environ.get("READER_ENDPOINT", PRIMARY_ENDPOINT) -REDIS_PORT = os.environ.get("REDIS_PORT", 6379) +REDIS_PORT = int(os.environ.get("REDIS_PORT", "6379")) REDIS_TOKEN = os.environ.get("REDIS_TOKEN") REDIS_SSL = os.environ.get("REDIS_SSL") class RedisCache: - """ - Dictionary-like implementation for caching requests in Redis. - """ + """Dictionary-like implementation for caching requests in Redis.""" def __init__(self): self.wr_cache = self._get_connection(PRIMARY_ENDPOINT) @@ -66,10 +62,7 @@ def __sizeof__(self): return self.ro_cache.info()["used_memory"] def _scan_iter(self): - """ - Make an iterator so that the client doesn't need to remember - the cursor position. - """ + """Make an iterator so that the client doesn't need to remember the cursor position.""" cursor = "0" while cursor != 0: cursor, data = self.wr_cache.scan(cursor) @@ -93,4 +86,4 @@ def _serialize(item): @staticmethod def _deserialize(item): """Deserialize items stored in Redis""" - return pickle.loads(item) + return pickle.loads(item) # noqa: S301 diff --git a/ghmirror/data_structures/requests_cache.py b/ghmirror/data_structures/requests_cache.py index 717da9b..d7a1f3a 100644 --- a/ghmirror/data_structures/requests_cache.py +++ b/ghmirror/data_structures/requests_cache.py @@ -12,9 +12,7 @@ # Copyright: Red Hat Inc. 2020 # Author: Maha Ashour -""" -Implements caching backend -""" +"""Implements caching backend""" import os @@ -25,9 +23,7 @@ class RequestsCache: - """ - Instantiates either a InMemoryCache or a Redis Cache object - """ + """Instantiates either a InMemoryCache or a Redis Cache object""" def __new__(cls, *args, **kwargs): if CACHE_TYPE == "redis": diff --git a/ghmirror/decorators/checks.py b/ghmirror/decorators/checks.py index 267f5f5..3cf7d9f 100644 --- a/ghmirror/decorators/checks.py +++ b/ghmirror/decorators/checks.py @@ -12,9 +12,7 @@ # Copyright: Red Hat Inc. 2020 # Author: Amador Pahim -""" -Contains all the required verification -""" +"""Contains all the required verification""" import os from functools import wraps @@ -31,9 +29,10 @@ def check_user(function): - """ - Checks whether the user is a member of one of the authorized users, - if no authorized users set, only cache user info. + """Check if the user is authorized to use the github-mirror. + + Checks whether the user is a member of one of the authorized users, if no + authorized users set, only cache user info. """ @wraps(function) diff --git a/ghmirror/decorators/metrics.py b/ghmirror/decorators/metrics.py index c1eb483..042141c 100644 --- a/ghmirror/decorators/metrics.py +++ b/ghmirror/decorators/metrics.py @@ -12,9 +12,7 @@ # Copyright: Red Hat Inc. 2020 # Author: Amador Pahim -""" -Metrics decorators. -""" +"""Metrics decorators.""" import time from functools import wraps @@ -30,10 +28,7 @@ def requests_metrics(function): - """ - Decorator to collect metrics from the request and populate the - StatsCache object. - """ + """Collect metrics from the request and populate the StatsCache object.""" @wraps(function) def wrapper(*args, **kwargs): diff --git a/ghmirror/utils/extensions.py b/ghmirror/utils/extensions.py index a3d1d34..817843a 100644 --- a/ghmirror/utils/extensions.py +++ b/ghmirror/utils/extensions.py @@ -1,6 +1,4 @@ -""" -Module to create a requests session that will be used to make all the requests to the GitHub API. -""" +"""Module to create a requests session that will be used to make all the requests to the GitHub API.""" import requests diff --git a/ghmirror/utils/wait.py b/ghmirror/utils/wait.py index d931caa..ab17553 100644 --- a/ghmirror/utils/wait.py +++ b/ghmirror/utils/wait.py @@ -1,13 +1,10 @@ -""" -Functions to help waiting for a given state -""" +"""Functions to help waiting for a given state""" import time def wait_for(func, timeout, first=0.0, step=1.0, args=None, kwargs=None): - """ - Wait until func() evaluates to True. + """Wait until func() evaluates to True. If func() evaluates to True before timeout expires, return the value of func(). Otherwise return None. diff --git a/pr_check.sh b/pr_check.sh index bba87c1..45b9a88 100755 --- a/pr_check.sh +++ b/pr_check.sh @@ -3,4 +3,3 @@ IMAGE_TEST=ghmirror-test docker build -t ${IMAGE_TEST} -f Dockerfile --target test . -docker run --rm ${IMAGE_TEST} diff --git a/pyproject.toml b/pyproject.toml index 74cf2a1..54800cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,33 +1,113 @@ +[project] +name = "github-mirror" +version = "0.1.0" +description = "GitHub API mirror that caches the responses and implements conditional requests, serving the client with the cached responses when the GitHub API replies with a 304 HTTP code, reducing the number of API calls, making a more efficient use of the GitHub API rate limit." +authors = [ + # Feel free to add or change authors + { name = "Red Hat Application SRE Team", email = "sd-app-sre@redhat.com" }, +] +license = { text = "GPLv2+" } +readme = "README.md" +requires-python = "~= 3.11.0" +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Environment :: Web Environment", + "Framework :: Flask", + "Intended Audience :: Developers", + "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", + "Natural Language :: English", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", +] +dependencies = [ + # 'uv add/remove XXX' to add and remove dependencies + "Flask ~=3.1.0", + "requests ~=2.32.3", + "prometheus_client ~=0.20", + "gunicorn ~=22.0.0", + "redis ~=5.0.4", +] + +[project.urls] +homepage = "https://github.com/app-sre/github-mirror" +repository = "https://github.com/app-sre/github-mirror" +documentation = "https://github.com/app-sre/github-mirror" + +[dependency-groups] +dev = [ + # Development dependencies + "ruff ~=0.8", + "mypy ~=1.13", + "pytest ~=8.2.0", + "pytest-cov ~=5.0.0", + "pytest-forked ~=1.6.0", + "types-requests>=2.32.0.20241016", +] + # Ruff configuration [tool.ruff] line-length = 88 -target-version = 'py311' src = ["ghmirror"] extend-exclude = [ - ".local", # used by poetry in local venv - ".cache", # used by poetry in local venv + # exclude some common cache and tmp directories + ".local", + ".cache", + "tmp", ] fix = true [tool.ruff.lint] preview = true -# defaults are ["E4", "E7", "E9", "F"] -extend-select = [ - # flake8 default rules - "E1", # preview rule - "E2", # preview rule - "W", - # isort - "I", - # pylint - "PL", - # pyupgrade - "UP", -] +select = ["ALL"] ignore = [ + "CPY", # Missing copyright header + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "D203", # 1 blank line required before class docstring + "D211", # No blank lines allowed before class docstring + "D212", # multi-line-summary-first-line + "D213", # multi-line-summary-second-line + "D4", # Doc string style + "E501", # Line too long + "G004", # Logging statement uses f-string "PLR0904", # Too many public methods "PLR0913", # Too many arguments "PLR0917", # Too many positional arguments + "S101", # Use of assert detected. Pytest uses assert + "S404", # subprocess import + "EM101", # Exception must not use a string literal, assign to variable first + "EM102", # Exception must not use an f-string literal, assign to variable first + "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes + "S324", # sha1 hash + "S403", # pickle usage + "TRY003", # Avoid specifying long messages outside the exception class + "TRY300", # try-consider-else + # pydoclint + "DOC", + # May conflict with the formatter, https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q", + "COM812", + "COM819", + "ISC001", + # Room for future improvements and refactoring + "ANN", # Missing annotation + "PT", # Use PyTest stuff instead unittest + "RUF012", # need type annotations + ] [tool.ruff.format] preview = true diff --git a/requirements-check.txt b/requirements-check.txt deleted file mode 100644 index f48b0a6..0000000 --- a/requirements-check.txt +++ /dev/null @@ -1,4 +0,0 @@ -pytest~=7.2 -pytest-cov==2.12.1 -pytest-xdist==1.34.0 -ruff==0.8.0 diff --git a/setup.py b/setup.py deleted file mode 100644 index 6ec6e33..0000000 --- a/setup.py +++ /dev/null @@ -1,36 +0,0 @@ -from setuptools import find_packages, setup - -setup( - name="ghmirror", - packages=find_packages(), - version=open("VERSION", encoding="locale").read().strip(), - author="Red Hat Application SRE Team", - author_email="sd-app-sre@redhat.com", - description="GitHub API mirror that caches the responses and implements " - "conditional requests, serving the client with the cached " - "responses when the GitHub API replies with a 304 HTTP " - "code, reducing the number of API calls, making a more " - "efficient use of the GitHub API rate limit.", - python_requires=">=3.11", - license="GPLv2+", - classifiers=[ - "Development Status :: 2 - Pre-Alpha", - "Environment :: Web Environment", - "Framework :: Flask", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", - "Natural Language :: English", - "Operating System :: POSIX :: Linux", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", - ], - install_requires=[ - "Flask~=3.1.0", - "requests~=2.32.3", - "prometheus_client~=0.20", - "gunicorn==22.0.0", - "redis~=5.0.4", - ], -) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/functional/__init__.py b/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_requests.py b/tests/unit/test_requests.py index dabb6c8..8c9bff6 100644 --- a/tests/unit/test_requests.py +++ b/tests/unit/test_requests.py @@ -1,3 +1,4 @@ +# ruff: noqa: SLF001 from random import randint from unittest import ( TestCase, @@ -18,11 +19,10 @@ class TestStatsCache(TestCase): - # pylint: disable=W0212 def test_shared_state(self): stats_cache_01 = StatsCache() with pytest.raises(AttributeError) as e_info: - stats_cache_01.foo + stats_cache_01.foo # noqa: B018 self.assertIn("object has no attribute", e_info.message) self.assertEqual(stats_cache_01.counter._value._value, 0) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b41e805 --- /dev/null +++ b/uv.lock @@ -0,0 +1,440 @@ +version = 1 +requires-python = ">=3.11.0, <3.12" + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233 }, +] + +[[package]] +name = "blinker" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458 }, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "click" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.6.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/75/aecfd0a3adbec6e45753976bc2a9fed62b42cea9a206d10fd29244a77953/coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc", size = 801425 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/9f/e98211980f6e2f439e251737482aa77906c9b9c507824c71a2ce7eea0402/coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4", size = 207093 }, + { url = "https://files.pythonhosted.org/packages/fd/c7/8bab83fb9c20f7f8163c5a20dcb62d591b906a214a6dc6b07413074afc80/coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94", size = 207536 }, + { url = "https://files.pythonhosted.org/packages/1e/d6/00243df625f1b282bb25c83ce153ae2c06f8e7a796a8d833e7235337b4d9/coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4", size = 239482 }, + { url = "https://files.pythonhosted.org/packages/1e/07/faf04b3eeb55ffc2a6f24b65dffe6e0359ec3b283e6efb5050ea0707446f/coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1", size = 236886 }, + { url = "https://files.pythonhosted.org/packages/43/23/c79e497bf4d8fcacd316bebe1d559c765485b8ec23ac4e23025be6bfce09/coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb", size = 238749 }, + { url = "https://files.pythonhosted.org/packages/b5/e5/791bae13be3c6451e32ef7af1192e711c6a319f3c597e9b218d148fd0633/coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8", size = 237679 }, + { url = "https://files.pythonhosted.org/packages/05/c6/bbfdfb03aada601fb8993ced17468c8c8e0b4aafb3097026e680fabb7ce1/coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a", size = 236317 }, + { url = "https://files.pythonhosted.org/packages/67/f9/f8e5a4b2ce96d1b0e83ae6246369eb8437001dc80ec03bb51c87ff557cd8/coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0", size = 237084 }, + { url = "https://files.pythonhosted.org/packages/f0/70/b05328901e4debe76e033717e1452d00246c458c44e9dbd893e7619c2967/coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801", size = 209638 }, + { url = "https://files.pythonhosted.org/packages/70/55/1efa24f960a2fa9fbc44a9523d3f3c50ceb94dd1e8cd732168ab2dc41b07/coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9", size = 210506 }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "flask" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/50/dff6380f1c7f84135484e176e0cac8690af72fa90e932ad2a0a60e28c69b/flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac", size = 680824 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/47/93213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a/flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136", size = 102979 }, +] + +[[package]] +name = "github-mirror" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flask" }, + { name = "gunicorn" }, + { name = "prometheus-client" }, + { name = "redis" }, + { name = "requests" }, +] + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-forked" }, + { name = "ruff" }, + { name = "types-requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "flask", specifier = "~=3.1.0" }, + { name = "gunicorn", specifier = "~=22.0.0" }, + { name = "prometheus-client", specifier = "~=0.20" }, + { name = "redis", specifier = "~=5.0.4" }, + { name = "requests", specifier = "~=2.32.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = "~=1.13" }, + { name = "pytest", specifier = "~=8.2.0" }, + { name = "pytest-cov", specifier = "~=5.0.0" }, + { name = "pytest-forked", specifier = "~=1.6.0" }, + { name = "ruff", specifier = "~=0.8" }, + { name = "types-requests", specifier = ">=2.32.0.20241016" }, +] + +[[package]] +name = "gunicorn" +version = "22.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/88/e2f93c5738a4c1f56a458fc7a5b1676fc31dcdbb182bef6b40a141c17d66/gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63", size = 3639760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/97/6d610ae77b5633d24b69c2ff1ac3044e0e565ecbd1ec188f02c45073054c/gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", size = 84443 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 }, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, +] + +[[package]] +name = "mypy" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 }, + { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 }, + { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 }, + { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 }, + { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 }, + { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "prometheus-client" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/54/a369868ed7a7f1ea5163030f4fc07d85d22d7a1d270560dab675188fb612/prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e", size = 78634 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/2d/46ed6436849c2c88228c3111865f44311cff784b4aabcdef4ea2545dbc3d/prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166", size = 54686 }, +] + +[[package]] +name = "py" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/ff/fec109ceb715d2a6b4c4a85a61af3b40c723a961e8828319fbcb15b868dc/py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", size = 207796 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708 }, +] + +[[package]] +name = "pytest" +version = "8.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/58/e993ca5357553c966b9e73cb3475d9c935fe9488746e13ebdf9b80fae508/pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977", size = 1427980 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/e7/81ebdd666d3bff6670d27349b5053605d83d55548e6bd5711f3b0ae7dd23/pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343", size = 339873 }, +] + +[[package]] +name = "pytest-cov" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 }, +] + +[[package]] +name = "pytest-forked" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/c9/93ad2ba2413057ee694884b88cf7467a46c50c438977720aeac26e73fdb7/pytest-forked-1.6.0.tar.gz", hash = "sha256:4dafd46a9a600f65d822b8f605133ecf5b3e1941ebb3588e943b4e3eb71a5a3f", size = 9977 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/af/9c0bda43e486a3c9bf1e0f876d0f241bc3f229d7d65d09331a0868db9629/pytest_forked-1.6.0-py3-none-any.whl", hash = "sha256:810958f66a91afb1a1e2ae83089d8dc1cd2437ac96b12963042fbb9fb4d16af0", size = 4897 }, +] + +[[package]] +name = "redis" +version = "5.0.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-timeout", marker = "python_full_version < '3.11.3'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/10/defc227d65ea9c2ff5244645870859865cba34da7373477c8376629746ec/redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870", size = 4595651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/d1/19a9c76811757684a0f74adc25765c8a901d67f9f6472ac9d57c844a23c8/redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4", size = 255608 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "ruff" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/d0/8ff5b189d125f4260f2255d143bf2fa413b69c2610c405ace7a0a8ec81ec/ruff-0.8.1.tar.gz", hash = "sha256:3583db9a6450364ed5ca3f3b4225958b24f78178908d5c4bc0f46251ccca898f", size = 3313222 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/d6/1a6314e568db88acdbb5121ed53e2c52cebf3720d3437a76f82f923bf171/ruff-0.8.1-py3-none-linux_armv6l.whl", hash = "sha256:fae0805bd514066f20309f6742f6ee7904a773eb9e6c17c45d6b1600ca65c9b5", size = 10532605 }, + { url = "https://files.pythonhosted.org/packages/89/a8/a957a8812e31facffb6a26a30be0b5b4af000a6e30c7d43a22a5232a3398/ruff-0.8.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8a4f7385c2285c30f34b200ca5511fcc865f17578383db154e098150ce0a087", size = 10278243 }, + { url = "https://files.pythonhosted.org/packages/a8/23/9db40fa19c453fabf94f7a35c61c58f20e8200b4734a20839515a19da790/ruff-0.8.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd054486da0c53e41e0086e1730eb77d1f698154f910e0cd9e0d64274979a209", size = 9917739 }, + { url = "https://files.pythonhosted.org/packages/e2/a0/6ee2d949835d5701d832fc5acd05c0bfdad5e89cfdd074a171411f5ccad5/ruff-0.8.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2029b8c22da147c50ae577e621a5bfbc5d1fed75d86af53643d7a7aee1d23871", size = 10779153 }, + { url = "https://files.pythonhosted.org/packages/7a/25/9c11dca9404ef1eb24833f780146236131a3c7941de394bc356912ef1041/ruff-0.8.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2666520828dee7dfc7e47ee4ea0d928f40de72056d929a7c5292d95071d881d1", size = 10304387 }, + { url = "https://files.pythonhosted.org/packages/c8/b9/84c323780db1b06feae603a707d82dbbd85955c8c917738571c65d7d5aff/ruff-0.8.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:333c57013ef8c97a53892aa56042831c372e0bb1785ab7026187b7abd0135ad5", size = 11360351 }, + { url = "https://files.pythonhosted.org/packages/6b/e1/9d4bbb2ace7aad14ded20e4674a48cda5b902aed7a1b14e6b028067060c4/ruff-0.8.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:288326162804f34088ac007139488dcb43de590a5ccfec3166396530b58fb89d", size = 12022879 }, + { url = "https://files.pythonhosted.org/packages/75/28/752ff6120c0e7f9981bc4bc275d540c7f36db1379ba9db9142f69c88db21/ruff-0.8.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b12c39b9448632284561cbf4191aa1b005882acbc81900ffa9f9f471c8ff7e26", size = 11610354 }, + { url = "https://files.pythonhosted.org/packages/ba/8c/967b61c2cc8ebd1df877607fbe462bc1e1220b4a30ae3352648aec8c24bd/ruff-0.8.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:364e6674450cbac8e998f7b30639040c99d81dfb5bbc6dfad69bc7a8f916b3d1", size = 12813976 }, + { url = "https://files.pythonhosted.org/packages/7f/29/e059f945d6bd2d90213387b8c360187f2fefc989ddcee6bbf3c241329b92/ruff-0.8.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b22346f845fec132aa39cd29acb94451d030c10874408dbf776af3aaeb53284c", size = 11154564 }, + { url = "https://files.pythonhosted.org/packages/55/47/cbd05e5a62f3fb4c072bc65c1e8fd709924cad1c7ec60a1000d1e4ee8307/ruff-0.8.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2f2f7a7e7648a2bfe6ead4e0a16745db956da0e3a231ad443d2a66a105c04fa", size = 10760604 }, + { url = "https://files.pythonhosted.org/packages/bb/ee/4c3981c47147c72647a198a94202633130cfda0fc95cd863a553b6f65c6a/ruff-0.8.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:adf314fc458374c25c5c4a4a9270c3e8a6a807b1bec018cfa2813d6546215540", size = 10391071 }, + { url = "https://files.pythonhosted.org/packages/6b/e6/083eb61300214590b188616a8ac6ae1ef5730a0974240fb4bec9c17de78b/ruff-0.8.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a885d68342a231b5ba4d30b8c6e1b1ee3a65cf37e3d29b3c74069cdf1ee1e3c9", size = 10896657 }, + { url = "https://files.pythonhosted.org/packages/77/bd/aacdb8285d10f1b943dbeb818968efca35459afc29f66ae3bd4596fbf954/ruff-0.8.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d2c16e3508c8cc73e96aa5127d0df8913d2290098f776416a4b157657bee44c5", size = 11228362 }, + { url = "https://files.pythonhosted.org/packages/39/72/fcb7ad41947f38b4eaa702aca0a361af0e9c2bf671d7fd964480670c297e/ruff-0.8.1-py3-none-win32.whl", hash = "sha256:93335cd7c0eaedb44882d75a7acb7df4b77cd7cd0d2255c93b28791716e81790", size = 8803476 }, + { url = "https://files.pythonhosted.org/packages/e4/ea/cae9aeb0f4822c44651c8407baacdb2e5b4dcd7b31a84e1c5df33aa2cc20/ruff-0.8.1-py3-none-win_amd64.whl", hash = "sha256:2954cdbe8dfd8ab359d4a30cd971b589d335a44d444b6ca2cb3d1da21b75e4b6", size = 9614463 }, + { url = "https://files.pythonhosted.org/packages/eb/76/fbb4bd23dfb48fa7758d35b744413b650a9fd2ddd93bca77e30376864414/ruff-0.8.1-py3-none-win_arm64.whl", hash = "sha256:55873cc1a473e5ac129d15eccb3c008c096b94809d693fc7053f588b67822737", size = 8959621 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "types-requests" +version = "2.32.0.20241016" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/3c/4f2a430c01a22abd49a583b6b944173e39e7d01b688190a5618bd59a2e22/types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95", size = 18065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/01/485b3026ff90e5190b5e24f1711522e06c79f4a56c8f4b95848ac072e20f/types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747", size = 15836 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498 }, +]