diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 3f886c84..00000000 --- a/.coveragerc +++ /dev/null @@ -1,3 +0,0 @@ -[run] -source = openedx_webhooks -branch = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4338588..4d935524 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,41 +1,48 @@ -name: Python CI +name: CI on: push: - branches: - - master + branches: [master] pull_request: - branches: - - '**' + # Allow this workflow to be called from other workflows + workflow_call: jobs: run_tests: - name: Tests - runs-on: ${{ matrix.os }} + # matrix.job-name overrides the display name for specific entries; used to + # preserve "Tests (ubuntu-latest, 3.12)" which is a required branch- + # protection status check on the main branch and cannot be renamed there. + name: ${{ matrix.job-name || matrix.toxenv }} + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - os: - - ubuntu-latest - python-version: - - "3.12" + python-version: ["3.12"] + toxenv: [lint, mypy, docs, py312-test] + include: + - toxenv: py312-test + job-name: "Tests (ubuntu-latest, 3.12)" steps: - - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - name: setup python - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 - with: - python-version: ${{ matrix.python-version }} + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Install dependencies - run: pip install -r requirements/test.txt + - name: Install uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + enable-cache: true + python-version: "${{ matrix.python-version }}" - - name: Run Tests - run: | - make test - cd docs && make html + - name: Install CI dependencies + run: uv sync --group ci - - name: Run Coverage - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true + - name: Run tox + run: uv run tox -e ${{ matrix.toxenv }} + + - name: Upload coverage to Codecov + if: matrix.toxenv == 'py312-test' + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests + fail_ci_if_error: true diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index fa4b3ac2..00000000 --- a/.isort.cfg +++ /dev/null @@ -1,4 +0,0 @@ -[settings] -line_length=79 -multi_line_output=5 -not_skip=__init__.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c3d355eb..0ee0cb5f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -16,4 +16,7 @@ build: python: install: - - requirements: requirements/dev.txt + - method: uv + command: sync + groups: + - doc diff --git a/Makefile b/Makefile index ae12f9a1..f6a3b9da 100644 --- a/Makefile +++ b/Makefile @@ -1,97 +1,53 @@ +.PHONY: help requirements upgrade upgrade-package lint format test mypy docs clean +.PHONY: deploy-configure deploy-check deploy-stage deploy-stage-branch deploy-prod + .DEFAULT_GOAL := help help: ## Display this help message @echo "Please use \`make ' where is one of the following:" @awk -F ':.*?## ' '/^[a-zA-Z]/ && NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort -check-setup.py: ## Check setup - python setup.py check -r -s - clean: ## Clean cache, test, and build directories - -rm -rf .cache build dist *.egg-info .coverage htmlcov docs/_build prof + -rm -rf .cache build dist *.egg-info .coverage htmlcov docs/_build prof .pytest_cache .ruff_cache -testschema: ## Install a schema under test. - # Get the version of repo-tools-data-schema that corresponds to our branch. - pip uninstall -y repo-tools-data-schema - pip install -U git+https://github.com/openedx/repo-tools-data-schema.git@$$(git rev-parse --abbrev-ref HEAD) +testschema: ## Install the repo-tools-data-schema version matching the current branch + uv pip install -U "repo-tools-data-schema @ git+https://github.com/openedx/repo-tools-data-schema.git@$$(git rev-parse --abbrev-ref HEAD)" -TEST_FLAGS = $(TEST_ARGS) -rxefs --cov=openedx_webhooks --cov=tests --cov-report= +test: ## Run tests + tox -e py312-test -test: ## Run tests - pytest $(TEST_FLAGS) --cov-context=test - coverage html --show-contexts - coverage xml +fulltest: ## Run tests including flaky GitHub emulation + uv run pytest -rxefs --cov=openedx_webhooks --cov=tests --cov-report= + uv run pytest -rxefs --cov=openedx_webhooks --cov=tests --cov-report= --cov-append -m flaky_github --disable-warnings --percent-404=1 --count=100 + uv run coverage html -fulltest: ## Run tests with randomness to emulate flaky GitHub - pytest $(TEST_FLAGS) - pytest $(TEST_FLAGS) --cov-append -m flaky_github --disable-warnings --percent-404=1 --count=100 - coverage html - -test-html-coverage-report: test ## Run tests and show coverage report in browser +test-html-coverage-report: test ## Run tests and open coverage report in browser open htmlcov/index.html -pylint: ## Run pylint - -pylint --rcfile=pylintrc openedx_webhooks tests setup.py - -TYPEABLE = openedx_webhooks tests -mypy: ## Run mypy to check type annotations - -mypy $(TYPEABLE) - -PIP_COMPILE = pip-compile --allow-unsafe --resolver=backtracking ${COMPILE_OPTS} -compile-requirements: export CUSTOM_COMPILE_COMMAND=make upgrade -compile-requirements: ## Update the requirements/*.txt files with the latest packages satisfying requirements/*.in - pip install -qr requirements/pip-tools.txt - # Make sure to compile files after any other files they include! - $(PIP_COMPILE) --rebuild -o requirements/pip.txt requirements/pip.in - $(PIP_COMPILE) -o requirements/pip-tools.txt requirements/pip-tools.in - pip install -qr requirements/pip.txt - pip install -qr requirements/pip-tools.txt - $(PIP_COMPILE) -o requirements/base.txt requirements/base.in - $(PIP_COMPILE) -o requirements/test.txt requirements/test.in - $(PIP_COMPILE) -o requirements/dev.txt requirements/dev.in - $(PIP_COMPILE) -o requirements/doc.txt requirements/doc.in - -upgrade: ## Update the requirements/*.txt files with the latest packages satisfying requirements/*.in - $(MAKE) compile-requirements COMPILE_OPTS="--upgrade" - -upgrade-package: ## Update just one package to the latest usable release - @test -n "$(package)" || { echo "\nUsage: make upgrade-package package=...\n"; exit 1; } - $(MAKE) compile-requirements COMPILE_OPTS="--upgrade-package $(package)" - -PRIVATE_IN = requirements/private.in -PRIVATE_OUT = requirements/private.txt - -pip-compile: ## Compile Python requirements without upgrading - pip install --no-cache-dir -q pip-tools - pip-compile requirements/base.in - pip-compile requirements/dev.in - pip-compile requirements/doc.in - pip-compile requirements/test.in -ifneq (, $(wildcard $(PRIVATE_IN))) - pip-compile $(PRIVATE_IN) -else -endif +lint: ## Run linting checks + tox -e lint -pip-compile-upgrade: ## Compile and upgrade Python requirements - pip install --no-cache-dir -q pip-tools - pip-compile -U requirements/base.in - pip-compile -U requirements/dev.in - pip-compile -U requirements/doc.in - pip-compile -U requirements/test.in -ifneq (, $(wildcard $(PRIVATE_IN))) - pip-compile -U $(PRIVATE_IN) -endif +format: ## Auto-fix formatting and import order issues + uv run ruff check --fix . + uv run ruff format . -install-dev-requirements: ## Install development requirements - pip install --no-cache-dir -q pip-tools -ifneq (, $(wildcard $(PRIVATE_OUT))) - pip-sync $(PRIVATE_OUT) -else - pip-sync requirements/dev.txt -endif - @# This run of mypy is to discover the missing type stubs, then we install them - -mypy $(TYPEABLE) > /dev/null - mypy --install-types --non-interactive +mypy: ## Run mypy type checks + tox -e mypy + +docs: ## Build documentation + tox -e docs + +upgrade: ## Upgrade and regenerate pinned dependencies + uv run --with edx-lint edx_lint write_uv_constraints pyproject.toml + uv lock --upgrade + +upgrade-package: ## Update just one package to the latest usable release + @test -n "$(package)" || { echo "\nUsage: make upgrade-package package=...\n"; exit 1; } + uv lock --upgrade-package $(package) + +requirements: ## Sync dev dependencies + uv sync --group dev + uv tool install tox --with tox-uv DEPLOY_PROD_APP=openedx-webhooks DEPLOY_STAGING_APP=openedx-webhooks-staging diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..4ef8780d --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +# Codecov configuration +# https://docs.codecov.com/docs/codecovyml-reference diff --git a/docs/conf.py b/docs/conf.py index 70203336..905c2837 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,213 +16,207 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinxcontrib.httpdomain', - 'sphinxcontrib.autohttp.flask', + "sphinx.ext.autodoc", + "sphinxcontrib.httpdomain", + "sphinxcontrib.autohttp.flask", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Open edX Webhooks Bot' -copyright = u'2016–2020, edX Inc.' +project = "Open edX Webhooks Bot" +copyright = "2016–2020, edX Inc." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.1' +version = "0.1" # The full version, including alpha/beta/rc tags. -release = '0.1' +release = "0.1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'OpenedXWebhooksBotdoc' +htmlhelp_basename = "OpenedXWebhooksBotdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'OpenedXWebhooksBot.tex', u'Open edX Webhooks Bot Documentation', - u'edX Inc.', 'manual'), + ("index", "OpenedXWebhooksBot.tex", "Open edX Webhooks Bot Documentation", "edX Inc.", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'openedxwebhooksbot', u'Open edX Webhooks Bot Documentation', - [u'edX Inc.'], 1) -] +man_pages = [("index", "openedxwebhooksbot", "Open edX Webhooks Bot Documentation", ["edX Inc."], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -231,16 +225,22 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'OpenedXWebhooksBot', u'Open edX Webhooks Bot Documentation', - u'edX Inc.', 'OpenedXWebhooksBot', 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "OpenedXWebhooksBot", + "Open edX Webhooks Bot Documentation", + "edX Inc.", + "OpenedXWebhooksBot", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/openedx_webhooks/__init__.py b/openedx_webhooks/__init__.py index 03773849..e9056209 100644 --- a/openedx_webhooks/__init__.py +++ b/openedx_webhooks/__init__.py @@ -3,18 +3,16 @@ import sys import traceback +import sentry_sdk from celery import Celery from flask import Flask from flask_sslify import SSLify -import sentry_sdk from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.flask import FlaskIntegration from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.utils import import_string -__version__ = "0.1.0" - -log_level = os.environ.get('LOGLEVEL', 'INFO').upper() +log_level = os.environ.get("LOGLEVEL", "INFO").upper() logger = logging.getLogger(__name__) handler = logging.StreamHandler(sys.stderr) handler.setLevel(log_level) @@ -30,14 +28,12 @@ def expand_config(name=None): if not name: name = "default" - return "openedx_webhooks.config.{classname}Config".format( - classname=name.capitalize(), - ) + return f"openedx_webhooks.config.{name.capitalize()}Config" def create_app(config=None): app = Flask(__name__) - app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore[method-assign] + app.wsgi_app = ProxyFix(app.wsgi_app) # type: ignore[method-assign] config = config or os.environ.get("OPENEDX_WEBHOOKS_CONFIG") or "default" # Instantiate the config object because we rely on the __init__ # function to translate config between heroku and what sqlalchemy wants @@ -50,10 +46,13 @@ def create_app(config=None): # attach our blueprints from .github_views import github_bp + app.register_blueprint(github_bp, url_prefix="/github") from .ui import ui as ui_blueprint + app.register_blueprint(ui_blueprint) from .tasks import tasks as tasks_blueprint + app.register_blueprint(tasks_blueprint, url_prefix="/tasks") return app @@ -70,8 +69,10 @@ def create_celery_app(app=None, config="worker"): app = app or create_app(config=config) celery.main = app.import_name celery.conf.update(app.config) - class ContextTask(celery.Task): # type: ignore[name-defined] + + class ContextTask(celery.Task): # type: ignore[name-defined] abstract = True + def __call__(self, *args, **kwargs): if "wsgi_environ" in kwargs: wsgi_environ = kwargs.pop("wsgi_environ") diff --git a/openedx_webhooks/auth.py b/openedx_webhooks/auth.py index d3cb707b..7b69a263 100644 --- a/openedx_webhooks/auth.py +++ b/openedx_webhooks/auth.py @@ -12,18 +12,13 @@ class BaseUrlSession(requests.Session): """ A requests Session class that applies a base URL to the requested URL. """ + def __init__(self, base_url): super().__init__() self.base_url = URLObject(base_url) - def request(self, method, url, data=None, headers=None, **kwargs): - return super().request( - method=method, - url=self.base_url.relative(url), - data=data, - headers=headers, - **kwargs - ) + def request(self, method, url, data=None, headers=None, **kwargs): # type: ignore[override] + return super().request(method=method, url=self.base_url.relative(url), data=data, headers=headers, **kwargs) def get_jira_session(jira_nick): @@ -38,7 +33,7 @@ def get_jira_session(jira_nick): jira_server = get_jira_server_info(jira_nick) session = BaseUrlSession(base_url=jira_server.server) session.auth = (jira_server.email, jira_server.token) - session.trust_env = False # prevent reading the local .netrc + session.trust_env = False # prevent reading the local .netrc return session @@ -48,5 +43,5 @@ def get_github_session(): """ session = BaseUrlSession(base_url="https://api.github.com") session.headers["Authorization"] = f"token {settings.GITHUB_PERSONAL_TOKEN}" - session.trust_env = False # prevent reading the local .netrc + session.trust_env = False # prevent reading the local .netrc return session diff --git a/openedx_webhooks/bot_comments.py b/openedx_webhooks/bot_comments.py index 419de5e8..a21c6fa2 100644 --- a/openedx_webhooks/bot_comments.py +++ b/openedx_webhooks/bot_comments.py @@ -5,9 +5,7 @@ import binascii import json import re - from enum import Enum, auto -from typing import Dict import arrow from flask import render_template @@ -26,10 +24,12 @@ "FIRST_TIME_CONTRIBUTOR", # Author has not previously committed to the repository. ) + class BotComment(Enum): """ Comments the bot can leave on pull requests. """ + WELCOME = auto() WELCOME_CLOSED = auto() NEED_CLA = auto() @@ -132,26 +132,29 @@ def github_blended_pr_comment(pull_request: PrDict) -> str: """ Create a Blended PR comment. """ - return render_template("github_blended_pr_comment.md.j2", + return render_template( + "github_blended_pr_comment.md.j2", user=pull_request["user"]["login"], is_draft=is_draft_pull_request(pull_request), ) SURVEY_URL = ( - 'https://docs.google.com/forms/d/e' - '/1FAIpQLSceJOyGJ6JOzfy6lyR3T7EW_71OWUnNQXp68Fymsk3MkNoSDg/viewform' - '?usp=pp_url' - '&entry.1671973413={repo_full_name}' - '&entry.867055334={pull_request_url}' - '&entry.1484655318={contributor_url}' - '&entry.752974735={created_at}' - '&entry.1917517419={closed_at}' - '&entry.2133058324={is_merged}' + "https://docs.google.com/forms/d/e" + "/1FAIpQLSceJOyGJ6JOzfy6lyR3T7EW_71OWUnNQXp68Fymsk3MkNoSDg/viewform" + "?usp=pp_url" + "&entry.1671973413={repo_full_name}" + "&entry.867055334={pull_request_url}" + "&entry.1484655318={contributor_url}" + "&entry.752974735={created_at}" + "&entry.1917517419={closed_at}" + "&entry.2133058324={is_merged}" ) + def _format_datetime(datetime_string): - return arrow.get(datetime_string).format('YYYY-MM-DD+HH:mm') + return arrow.get(datetime_string).format("YYYY-MM-DD+HH:mm") + def github_end_survey_comment(pull_request: PrDict) -> str: """ @@ -174,7 +177,7 @@ def github_end_survey_comment(pull_request: PrDict) -> str: ) -def jira_issue_comment(pull_request: PrDict, jira_id: JiraId) -> str: # pylint: disable=unused-argument +def jira_issue_comment(pull_request: PrDict, jira_id: JiraId) -> str: # pylint: disable=unused-argument """Render a comment about making a new Jira issue.""" jira_server = get_jira_server_info(jira_id.nick) body = render_template( @@ -187,7 +190,7 @@ def jira_issue_comment(pull_request: PrDict, jira_id: JiraId) -> str: # pylint return body -def no_contributions_thanks(pull_request: PrDict) -> str: # pylint: disable=unused-argument +def no_contributions_thanks(pull_request: PrDict) -> str: # pylint: disable=unused-argument """ Create a "no contributions" comment. """ @@ -219,7 +222,7 @@ def no_jira_server_comment(jira_nick: str) -> str: return body -def extract_data_from_comment(text: str) -> Dict: +def extract_data_from_comment(text: str) -> dict: """ Extract the data from a data HTML comment in the comment text. """ @@ -231,7 +234,7 @@ def extract_data_from_comment(text: str) -> Dict: return {} -def format_data_for_comment(data: Dict) -> str: +def format_data_for_comment(data: dict) -> str: """ Format a data dictionary for appending to a comment. """ diff --git a/openedx_webhooks/cla_check.py b/openedx_webhooks/cla_check.py index aca57fe7..d2a3fa32 100644 --- a/openedx_webhooks/cla_check.py +++ b/openedx_webhooks/cla_check.py @@ -2,15 +2,13 @@ Management of the CLA check (actually a commit status). """ -from typing import Dict, Optional - from openedx_webhooks.auth import get_github_session from openedx_webhooks.tasks import logger from openedx_webhooks.types import PrDict from openedx_webhooks.utils import log_check_response -def _get_latest_commit_for_pull_request(repo_name_full: str, number: int) -> Optional[str]: +def _get_latest_commit_for_pull_request(repo_name_full: str, number: int) -> str | None: """ Get the HEAD commit for a pull request. """ @@ -18,12 +16,12 @@ def _get_latest_commit_for_pull_request(repo_name_full: str, number: int) -> Opt response = get_github_session().get(url) log_check_response(response) data = response.json() - sha = data['head']['sha'] + sha = data["head"]["sha"] logger.debug("CLA: SHA %s", sha) return sha -def _get_commit_status_for_cla(url) -> Optional[Dict[str, str]]: +def _get_commit_status_for_cla(url) -> dict[str, str] | None: """ Send a GET request to the GitHub API to lookup the build status. @@ -35,18 +33,11 @@ def _get_commit_status_for_cla(url) -> Optional[Dict[str, str]]: log_check_response(response) data = response.json() logger.debug("CLA: GOT %s %s", url, data) - cla_statuses = [ - status - for status in data - if status['context'] == CLA_CONTEXT - ] + cla_statuses = [status for status in data if status["context"] == CLA_CONTEXT] status = None if cla_statuses: cla_status = cla_statuses[-1] - status = { - k: v for k, v in cla_status.items() - if k in ["context", "state", "description", "target_url"] - } + status = {k: v for k, v in cla_status.items() if k in ["context", "state", "description", "target_url"]} return status @@ -62,15 +53,15 @@ def _update_commit_status_for_cla(url, payload): return data -def cla_status_on_pr(pull_request: PrDict) -> Optional[Dict[str, str]]: +def cla_status_on_pr(pull_request: PrDict) -> dict[str, str] | None: """ Get the CLA status for a pull request. Returns: a dict with context, state, description, and target_url. """ - repo_name_full = pull_request['base']['repo']['full_name'] - number = pull_request['number'] + repo_name_full = pull_request["base"]["repo"]["full_name"] + number = pull_request["number"] sha = _get_latest_commit_for_pull_request(repo_name_full, number) if not sha: return None @@ -78,6 +69,7 @@ def cla_status_on_pr(pull_request: PrDict) -> Optional[Dict[str, str]]: status = _get_commit_status_for_cla(url) return status + # A status is a dict of values. We only have a few that we use, so build them # all here. CLA_CONTEXT = "openedx/cla" @@ -117,7 +109,7 @@ def cla_status_on_pr(pull_request: PrDict) -> Optional[Dict[str, str]]: } -def set_cla_status_on_pr(repo_name_full: str, number: int, status: Dict[str, str]) -> bool: +def set_cla_status_on_pr(repo_name_full: str, number: int, status: dict[str, str]) -> bool: """ Set the CLA check status on a pull request. @@ -133,7 +125,7 @@ def set_cla_status_on_pr(repo_name_full: str, number: int, status: Dict[str, str sha = _get_latest_commit_for_pull_request(repo_name_full, number) logger.debug("CLA: Update status to %r for commit %r", status, sha) payload = { - 'context': CLA_CONTEXT, + "context": CLA_CONTEXT, **status, } url = f"https://api.github.com/repos/{repo_name_full}/statuses/{sha}" diff --git a/openedx_webhooks/config.py b/openedx_webhooks/config.py index be61f2f8..fa42b53f 100644 --- a/openedx_webhooks/config.py +++ b/openedx_webhooks/config.py @@ -8,8 +8,8 @@ class DefaultConfig: CELERY_TASK_SERIALIZER = "json" CELERY_RESULT_SERIALIZER = "json" CELERY_EAGER_PROPAGATES = True - BROKER_URL = os.environ.get('REDIS_TLS_URL', os.environ.get("REDIS_URL", "redis://")) - CELERY_RESULT_BACKEND = os.environ.get('REDIS_TLS_URL', os.environ.get("REDIS_URL", "redis://")) + BROKER_URL = os.environ.get("REDIS_TLS_URL", os.environ.get("REDIS_URL", "redis://")) + CELERY_RESULT_BACKEND = os.environ.get("REDIS_TLS_URL", os.environ.get("REDIS_URL", "redis://")) def __init__(self): # Don't require cert validation if usng redis over TLS because heroku redis uses self signed certs. @@ -21,9 +21,7 @@ def __init__(self): class WorkerConfig(DefaultConfig): - CELERY_IMPORTS = ( - 'openedx_webhooks.tasks.github', - ) + CELERY_IMPORTS = ("openedx_webhooks.tasks.github",) class DevelopmentConfig(DefaultConfig): diff --git a/openedx_webhooks/debug.py b/openedx_webhooks/debug.py index 9c6608f6..5fd26c73 100644 --- a/openedx_webhooks/debug.py +++ b/openedx_webhooks/debug.py @@ -14,11 +14,8 @@ def is_debug(module_name): def print_long(label, long_text): """Print a long data dump in a logging-safe way.""" data = base64.b85encode(gzip.compress(long_text.encode())).decode() - print( - f"{label}:", - "import base64,gzip;" + - f"print(gzip.decompress(base64.b85decode({data!r})).decode())" - ) + print(f"{label}:", "import base64,gzip;" + f"print(gzip.decompress(base64.b85decode({data!r})).decode())") + def print_long_json(label, jdata): """Like print_long, but for JSON data.""" diff --git a/openedx_webhooks/gh_projects.py b/openedx_webhooks/gh_projects.py index cae10070..27b72c15 100644 --- a/openedx_webhooks/gh_projects.py +++ b/openedx_webhooks/gh_projects.py @@ -2,8 +2,6 @@ Functions for working with GitHub projects with the GraphQL API. """ -from typing import Set - from glom import glom from openedx_webhooks.tasks import logger @@ -44,26 +42,20 @@ def pull_request_projects_info(pr: PrDict) -> list[PrGhProject]: example: [{"id": "some-id", "org": "login_id", "number": "1"}] """ - variables = glom(pr, { - "owner": "base.repo.owner.login", - "name": "base.repo.name", - "number": "number" - }) + variables = glom(pr, {"owner": "base.repo.owner.login", "name": "base.repo.name", "number": "number"}) logger.debug(f"Getting projects for PR: {variables}") data = graphql_query(query=PROJECTS_FOR_PR, variables=variables) projects = glom( data, ( "repository.pullRequest.projectItems.nodes", - [ - {"id": "id", "org": "project.owner.login", "number": "project.number"} - ] - ) + [{"id": "id", "org": "project.owner.login", "number": "project.number"}], + ), ) return projects -def pull_request_projects(pr: PrDict, projects: list[PrGhProject] | None = None) -> Set[GhProject]: +def pull_request_projects(pr: PrDict, projects: list[PrGhProject] | None = None) -> set[GhProject]: """ Helper method for expressing projects info as sets of tuples with owning org and number: {("openedx", 19)} @@ -203,6 +195,5 @@ def update_project_pr_custom_field(field_name: str, field_value, item_id: str, p field_type_name = target_field["dataType"].lower() field_type = value_graphql_type(field_type_name) graphql_query( - query=UPDATE_PROJECT_ITEM.format(fieldType=field_type, fieldTypeName=field_type_name), - variables=variables + query=UPDATE_PROJECT_ITEM.format(fieldType=field_type, fieldTypeName=field_type_name), variables=variables ) diff --git a/openedx_webhooks/github_views.py b/openedx_webhooks/github_views.py index f33042de..7767d9de 100644 --- a/openedx_webhooks/github_views.py +++ b/openedx_webhooks/github_views.py @@ -5,26 +5,26 @@ import logging from typing import cast -from flask import current_app as app from flask import Blueprint, jsonify, render_template, request +from flask import current_app as app from openedx_webhooks import celery from openedx_webhooks.auth import get_github_session from openedx_webhooks.debug import is_debug, print_long_json from openedx_webhooks.info import get_bot_username from openedx_webhooks.tasks.github import ( - pull_request_changed_task, rescan_repository, rescan_repository_task, + pull_request_changed_task, rescan_organization_task, + rescan_repository, + rescan_repository_task, ) -from openedx_webhooks.utils import ( - is_valid_payload, queue_task, requires_auth, sentry_extra_context -) +from openedx_webhooks.utils import is_valid_payload, queue_task, requires_auth, sentry_extra_context -github_bp = Blueprint('github_views', __name__) +github_bp = Blueprint("github_views", __name__) logger = logging.getLogger(__name__) -@github_bp.route('/hook-receiver', methods=('POST',)) +@github_bp.route("/hook-receiver", methods=("POST",)) def hook_receiver(): """ Process incoming GitHub webhook events. @@ -38,8 +38,8 @@ def hook_receiver(): A response, or Tuple[str, int]: Message payload and HTTP status code """ signature = request.headers.get("X-Hub-Signature") - secret = app.config.get('GITHUB_WEBHOOKS_SECRET') - if not is_valid_payload(secret, signature, request.data): # type: ignore[arg-type] + secret = app.config.get("GITHUB_WEBHOOKS_SECRET") + if not is_valid_payload(secret, signature, request.data): # type: ignore[arg-type] msg = "Rejecting because signature doesn't match!" logging.info(msg) return msg, 403 @@ -82,6 +82,7 @@ def hook_receiver(): # Ignore all other events. return "Thank you", 202 + # Actions on pull requests that we'll act on. PR_ACTIONS = { "opened", @@ -95,6 +96,7 @@ def hook_receiver(): "labeled", } + def handle_pull_request_event(event): """Handle a webhook event about a pull request.""" @@ -123,10 +125,9 @@ def handle_comment_event(event): pass case { - "issue": {"closed_at": closed}, + "issue": {"closed_at": closed}, "comment": {"created_at": commented}, - } if closed == commented: - + } if closed == commented: # This is a "Close with comment" comment. Don't do anything for the # comment, because we'll also get a "pull request closed" event at # the same time, and it will do whatever we need. @@ -170,21 +171,21 @@ def rescan(): repo = cast(str, request.form.get("repo")) inline = bool(request.form.get("inline", False)) - rescan_kwargs = dict( # pylint: disable=use-dict-literal - allpr=bool(request.form.get("allpr", False)), - dry_run=bool(request.form.get("dry_run", False)), - earliest=request.form.get("earliest", ""), - latest=request.form.get("latest", ""), - ) + rescan_kwargs = { + "allpr": bool(request.form.get("allpr", False)), + "dry_run": bool(request.form.get("dry_run", False)), + "earliest": request.form.get("earliest", ""), + "latest": request.form.get("latest", ""), + } - if repo.startswith('all:'): + if repo.startswith("all:"): if inline: return "Don't be silly." org = repo[4:] return queue_task(rescan_organization_task, org, **rescan_kwargs) elif inline: - return jsonify(rescan_repository(repo, **rescan_kwargs)) # type: ignore[arg-type] + return jsonify(rescan_repository(repo, **rescan_kwargs)) # type: ignore[arg-type] else: return queue_task(rescan_repository_task, repo, **rescan_kwargs) diff --git a/openedx_webhooks/info.py b/openedx_webhooks/info.py index 7236c9ca..e9324aaa 100644 --- a/openedx_webhooks/info.py +++ b/openedx_webhooks/info.py @@ -1,19 +1,21 @@ """ Get information about people, repos, orgs, pull requests, etc. """ -from collections import namedtuple + import csv import fnmatch import logging import re -from typing import Dict, Iterable, Literal, Optional +from collections import namedtuple +from collections.abc import Iterable +from typing import Literal import yaml from glom import glom from openedx_webhooks import settings from openedx_webhooks.auth import get_github_session -from openedx_webhooks.types import GhProject, JiraServer, PrDict, PrCommentDict, PrId +from openedx_webhooks.types import GhProject, JiraServer, PrCommentDict, PrDict, PrId from openedx_webhooks.utils import ( memoize, memoize_timed, @@ -30,10 +32,12 @@ def _github_file_url(repo_fullname: str, file_path: str) -> str: # uses master or main. return f"https://raw.githubusercontent.com/{repo_fullname}/HEAD/{file_path}" + def _read_yaml_data_file(filename): """Read a YAML file from openedx-webhooks-data.""" return yaml.safe_load(_read_data_file(filename)) + def _read_csv_data_file(filename): """ Reads a CSV file from openedx-webhooks-data. Returns a csv DictReader @@ -42,6 +46,7 @@ def _read_csv_data_file(filename): """ return csv.DictReader(_read_data_file(filename).splitlines()) + # Cache the webhooks data files, because every PR change reads them. @memoize_timed(minutes=15) def _read_data_file(filename): @@ -51,7 +56,7 @@ def _read_data_file(filename): return read_github_file("openedx/openedx-webhooks-data", filename) -def read_github_file(repo_fullname: str, file_path: str, not_there: Optional[str] = None) -> str: +def read_github_file(repo_fullname: str, file_path: str, not_there: str | None = None) -> str: """ Read a GitHub file from the main or master branch of a repo. @@ -69,7 +74,7 @@ def read_github_file(repo_fullname: str, file_path: str, not_there: Optional[str return _read_github_url(_github_file_url(repo_fullname, file_path), not_there) -def _read_github_url(url: str, not_there: Optional[str] = None) -> str: +def _read_github_url(url: str, not_there: str | None = None) -> str: """ Read the content of a GitHub URL. @@ -108,18 +113,24 @@ def get_people_file(): people_data_csv = _read_csv_data_file("salesforce-export.csv") # Simple assurance that the data is what we expect. assert people_data_csv.fieldnames == [ - "First Name", "Last Name", "Number of Active Ind. CLA Contracts", - "Title", "Account Name", "Number of Active Entity CLA Contracts", "GitHub Username","Is Core Contributor" + "First Name", + "Last Name", + "Number of Active Ind. CLA Contracts", + "Title", + "Account Name", + "Number of Active Entity CLA Contracts", + "GitHub Username", + "Is Core Contributor", ] people = {} for row in people_data_csv: - first_name = row['First Name'] - last_name = row['Last Name'] - acct_name = row['Account Name'] - github_username = row['GitHub Username'].lower() - is_cc = row['Is Core Contributor'] == '1' + first_name = row["First Name"] + last_name = row["Last Name"] + acct_name = row["Account Name"] + github_username = row["GitHub Username"].lower() + is_cc = row["Is Core Contributor"] == "1" people[github_username] = { "name": f"{first_name} {last_name}", @@ -127,11 +138,11 @@ def get_people_file(): } if acct_name == "Individual Contributors": - people[github_username]["agreement"] = 'individual' + people[github_username]["agreement"] = "individual" elif not acct_name: people[github_username]["agreement"] = "none" else: - people[github_username]["agreement"] = 'institution' + people[github_username]["agreement"] = "institution" people[github_username]["institution"] = acct_name return people @@ -159,6 +170,7 @@ def get_jira_info() -> dict[str, JiraServer]: class NoJiraServer(Exception): """Raised when there is no Jira with a given nickname.""" + class NoJiraMapping(Exception): """Raised when the repo isn't mapped to a Jira project.""" @@ -174,6 +186,7 @@ def get_jira_server_info(jira_nick: str) -> JiraServer: raise NoJiraServer(f"No Jira server configured with nick {jira_nick!r}") from exc return jira_server + def is_pr_author_cc(pull_request: PrDict) -> bool: """ Is this pull request's author a core contributor? @@ -219,13 +232,13 @@ def is_internal_pull_request(pull_request: PrDict) -> bool: # logic should be generalized, but this is good for now. PRIVATABLE_ORGS = {"edx"} + def is_private_repo_no_cla_pull_request(pull_request: PrDict) -> bool: """ Is this a private edX pull request? """ - return ( - pull_request["base"]["repo"]["owner"]["login"] in PRIVATABLE_ORGS and - pull_request["base"]["repo"].get("private", False) + return pull_request["base"]["repo"]["owner"]["login"] in PRIVATABLE_ORGS and pull_request["base"]["repo"].get( + "private", False ) @@ -243,7 +256,7 @@ def is_draft_pull_request(pull_request: PrDict) -> bool: return pull_request.get("draft", False) or bool(re.search(r"\b(WIP|wip)\b", pull_request["title"])) -def _pr_author_data(pull_request: PrDict) -> Optional[Dict]: +def _pr_author_data(pull_request: PrDict) -> dict | None: """ Get data about the author of the pull request, as of the creation of the pull request. @@ -257,6 +270,7 @@ def _pr_author_data(pull_request: PrDict) -> Optional[Dict]: NO_CONTRIBUTION_ORGS = {"edx"} + def repo_refuses_contributions(pull_request: PrDict) -> bool: """ Does this PR's repo accept contributions at all? @@ -276,7 +290,7 @@ def pull_request_has_cla(pull_request: PrDict) -> bool: return agreement != "none" -def get_blended_project_id(pull_request: PrDict) -> Optional[int]: +def get_blended_project_id(pull_request: PrDict) -> int | None: """ Find the blended project id in the pull request, if any. @@ -314,7 +328,7 @@ def get_bot_comments(prid: PrId) -> Iterable[PrCommentDict]: @memoize_timed(minutes=15) -def get_catalog_info(repo_fullname: str) -> Dict: +def get_catalog_info(repo_fullname: str) -> dict: """Get the parsed catalog-info.yaml data from a repo, or {} if missing.""" yml = read_github_file(repo_fullname, "catalog-info.yaml", not_there="{}") try: @@ -326,7 +340,7 @@ def get_catalog_info(repo_fullname: str) -> Dict: @memoize_timed(minutes=60) -def get_github_user_info(username: str) -> Dict | None: +def get_github_user_info(username: str) -> dict | None: """Get github user information""" resp = get_github_session().get(f"/users/{username}") if resp.ok: @@ -336,7 +350,7 @@ def get_github_user_info(username: str) -> Dict | None: Lifecycle = Literal["experimental", "production", "deprecated"] -RepoSpec: (str | None, Lifecycle | None, bool) = namedtuple('RepoSpec', ['owner', 'lifecycle', 'is_owner_individual']) +RepoSpec: (str | None, Lifecycle | None, bool) = namedtuple("RepoSpec", ["owner", "lifecycle", "is_owner_individual"]) def get_repo_spec(repo_full_name: str) -> RepoSpec: diff --git a/openedx_webhooks/settings.py b/openedx_webhooks/settings.py index 8375a3d7..2df6f98f 100644 --- a/openedx_webhooks/settings.py +++ b/openedx_webhooks/settings.py @@ -1,12 +1,11 @@ """Settings for how the webhook should behave.""" import os -from typing import Optional from openedx_webhooks.types import GhProject -def read_project_setting(setting_name: str) -> Optional[GhProject]: +def read_project_setting(setting_name: str) -> GhProject | None: """Read a project spec from a setting. Project number NUM in org ORG is specified as ``ORG:NUM``. diff --git a/openedx_webhooks/tasks/__init__.py b/openedx_webhooks/tasks/__init__.py index 02c05487..25d1660e 100644 --- a/openedx_webhooks/tasks/__init__.py +++ b/openedx_webhooks/tasks/__init__.py @@ -8,7 +8,6 @@ from openedx_webhooks import celery, log_level from openedx_webhooks.utils import requires_auth - # Set up Celery logging. logger = get_task_logger(__name__) logger.setLevel(log_level) @@ -19,28 +18,35 @@ # print_long("logging_tree output", logging_tree.format.build_description()) # create a Flask blueprint for getting task status info -tasks = Blueprint('tasks', __name__) +tasks = Blueprint("tasks", __name__) + -@tasks.route('/status/') +@tasks.route("/status/") @requires_auth def status(task_id): result = celery.AsyncResult(task_id) - return jsonify({ - "status": result.state, - "info": result.info, - }) + return jsonify( + { + "status": result.state, + "info": result.info, + } + ) -@tasks.route('/statusrepr/') + +@tasks.route("/statusrepr/") @requires_auth def statusrepr(task_id): """Get the status of a task, but repr() everything so we can see JSON failures from /status/""" result = celery.AsyncResult(task_id) - return jsonify({ - "status": repr(result.state), - "info": repr(result.info), - }) + return jsonify( + { + "status": repr(result.state), + "info": repr(result.info), + } + ) + -@tasks.route('/status/group:') +@tasks.route("/status/group:") @requires_auth def group_status(group_id): # NOTE: This will only work if the GroupResult @@ -56,12 +62,14 @@ def group_status(group_id): failed_task_ids.append(result.id) else: pending_task_ids.append(result.id) - return jsonify({ - "task_count": len(group_result.results), - "completed_task_count": len(completed_task_ids), - "completed_task_ids": completed_task_ids, - "failed_task_count": len(failed_task_ids), - "failed_task_info": failed_task_ids, - "pending_task_count": len(pending_task_ids), - "pending_task_info": pending_task_ids, - }) + return jsonify( + { + "task_count": len(group_result.results), + "completed_task_count": len(completed_task_ids), + "completed_task_ids": completed_task_ids, + "failed_task_count": len(failed_task_ids), + "failed_task_info": failed_task_ids, + "pending_task_count": len(pending_task_ids), + "pending_task_info": pending_task_ids, + } + ) diff --git a/openedx_webhooks/tasks/github.py b/openedx_webhooks/tasks/github.py index 621ff6b5..b72fbe72 100644 --- a/openedx_webhooks/tasks/github.py +++ b/openedx_webhooks/tasks/github.py @@ -4,8 +4,6 @@ import traceback -from typing import Dict, Set - from urlobject import URLObject from openedx_webhooks import celery @@ -13,11 +11,11 @@ from openedx_webhooks.info import is_internal_pull_request from openedx_webhooks.tasks import logger from openedx_webhooks.tasks.pr_tracking import ( - current_support_state, - desired_support_state, DryRunFixingActions, FixResult, PrTrackingFixer, + current_support_state, + desired_support_state, ) from openedx_webhooks.types import JiraId, PrDict from openedx_webhooks.utils import ( @@ -73,6 +71,7 @@ class PaginateCallback: """ A callback for paginated_get which updates the celery task with URL progress. """ + def __init__(self, task, meta): self.task = task self.meta = meta @@ -83,16 +82,13 @@ def __call__(self, response): current_page = int(current_url.query_dict.get("page", 1)) link_last = response.links.get("last") if link_last: - last_url = URLObject(link_last['url']) + last_url = URLObject(link_last["url"]) last_page = int(last_url.query_dict["page"]) else: last_page = current_page - state_meta = { - "current_page": current_page, - "last_page": last_page - } + state_meta = {"current_page": current_page, "last_page": last_page} state_meta.update(self.meta) - self.task.update_state(state='STARTED', meta=state_meta) + self.task.update_state(state="STARTED", meta=state_meta) @celery.task(bind=True) @@ -105,13 +101,13 @@ def rescan_repository_task(task, repo, allpr, dry_run, earliest, latest): def rescan_repository( - repo: str, - allpr: bool, - dry_run: bool = False, - earliest: str = "", - latest: str = "", - page_callback=None, - ) -> Dict: + repo: str, + allpr: bool, + dry_run: bool = False, + earliest: str = "", + latest: str = "", + page_callback=None, +) -> dict: """ Re-scans a single repo for external pull requests. @@ -132,8 +128,8 @@ def rescan_repository( state = "all" if allpr else "open" url = f"/repos/{repo}/pulls?state={state}" - changed: Dict[int, Set[JiraId]] = {} - errors: Dict[int, str] = {} + changed: dict[int, set[JiraId]] = {} + errors: dict[int, str] = {} dry_run_actions = {} # Pull requests before this will not be rescanned. Contractor messages @@ -164,7 +160,7 @@ def rescan_repository( pull_request = resp.json() result = pull_request_changed(pull_request, actions=actions) - except Exception: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except errors[pull_request["number"]] = traceback.format_exc() else: if result.changed_jira_issues: @@ -175,12 +171,10 @@ def rescan_repository( if not dry_run: logger.info( - "Changed {num} JIRA issues on repo {repo}. PRs are {prs}".format( - num=len(changed), repo=repo, prs=list(changed.keys()), - ), + f"Changed {len(changed)} JIRA issues on repo {repo}. PRs are {list(changed.keys())}", ) - info: Dict = { + info: dict = { "repo": repo, "changed": changed, "errors": errors, @@ -198,14 +192,15 @@ def rescan_organization_task(task, org, allpr, dry_run, earliest, latest): callback = PaginateCallback(task, meta) return rescan_organization(org, allpr, dry_run, earliest, latest, page_callback=callback) + def rescan_organization( - org: str, - allpr: bool = False, - dry_run: bool = False, - earliest: str = "", - latest: str = "", - page_callback=None, - ) -> Dict: + org: str, + allpr: bool = False, + dry_run: bool = False, + earliest: str = "", + latest: str = "", + page_callback=None, +) -> dict: """ Re-scan an entire organization. @@ -217,7 +212,7 @@ def rescan_organization( for irepo, repo in enumerate(repos): repo_name = repo["full_name"] if page_callback is not None: - page_callback.meta = {"repo": repo_name, "repo_num": f"{irepo+1}/{len(repos)}"} + page_callback.meta = {"repo": repo_name, "repo_num": f"{irepo + 1}/{len(repos)}"} info = rescan_repository(repo_name, allpr, dry_run, earliest, latest, page_callback=page_callback) if list(info) != ["repo"]: infos[repo_name] = info diff --git a/openedx_webhooks/tasks/jira_work.py b/openedx_webhooks/tasks/jira_work.py index bdc1f32e..210f59dc 100644 --- a/openedx_webhooks/tasks/jira_work.py +++ b/openedx_webhooks/tasks/jira_work.py @@ -2,7 +2,7 @@ Jira manipulations. """ -from typing import Any, Dict, List, Optional +from typing import Any from openedx_webhooks.auth import get_jira_session from openedx_webhooks.utils import ( @@ -11,16 +11,16 @@ def update_jira_issue( - jira_nick: str, - issue_key: str, - summary: Optional[str]=None, - description: Optional[str]=None, - labels: Optional[List[str]]=None, - ) -> None: + jira_nick: str, + issue_key: str, + summary: str | None = None, + description: str | None = None, + labels: list[str] | None = None, +) -> None: """ Update some fields on a Jira issue. """ - fields: Dict[str, Any] = {} + fields: dict[str, Any] = {} notify = "false" if summary is not None: fields["summary"] = summary diff --git a/openedx_webhooks/tasks/pr_tracking.py b/openedx_webhooks/tasks/pr_tracking.py index 2630879c..57cdd47e 100644 --- a/openedx_webhooks/tasks/pr_tracking.py +++ b/openedx_webhooks/tasks/pr_tracking.py @@ -9,7 +9,7 @@ import dataclasses import itertools from dataclasses import dataclass, field -from typing import Dict, List, Optional, Set, cast +from typing import cast from openedx_webhooks import settings from openedx_webhooks.auth import get_github_session, get_jira_session @@ -53,12 +53,12 @@ is_bot_pull_request, is_draft_pull_request, is_internal_pull_request, + is_pr_author_cc, is_private_repo_no_cla_pull_request, jira_details_for_pr, projects_for_pr, pull_request_has_cla, repo_refuses_contributions, - is_pr_author_cc, ) from openedx_webhooks.labels import ( GITHUB_CATEGORY_LABELS, @@ -84,12 +84,13 @@ class BotData: """ The data we store hidden in bot comments, to track our work. """ + # Is this a draft pull request? draft: bool = False # The Jira issues associated with the pull request. - jira_issues: Set[JiraId] = field(default_factory=set) + jira_issues: set[JiraId] = field(default_factory=set) # Jira nick labels that have created error comments. - jira_errors: Set[str] = field(default_factory=set) + jira_errors: set[str] = field(default_factory=set) def update(self, data: dict) -> None: """Add data from `data` to this BotData.""" @@ -106,32 +107,33 @@ class PrCurrentInfo: """ The current information we have for a pull request. """ - bot_comments: Set[BotComment] = field(default_factory=set) + + bot_comments: set[BotComment] = field(default_factory=set) # The text of the first bot comment. - bot_comment0_text: Optional[str] = None + bot_comment0_text: str | None = None # The comment id of the survey comment, if any. - bot_survey_comment_id: Optional[str] = None + bot_survey_comment_id: str | None = None # The last-seen state stored in the first bot comment. bot_data: BotData = field(default_factory=BotData) # The actual Jira issue id. Can differ from jira_mentioned_id if the # issue was moved, or can be None if the issue has been deleted. - jira_id: Optional[str] = None + jira_id: str | None = None # The actual set of labels on the pull request. - github_labels: Set[str] = field(default_factory=set) + github_labels: set[str] = field(default_factory=set) # The GitHub projects the PR is in. - github_projects: Set[GhProject] = field(default_factory=set) + github_projects: set[GhProject] = field(default_factory=set) # The GitHub projects the PR is in. github_projects_info: list[PrGhProject] = field(default_factory=list) # The status of the cla check. - cla_check: Optional[Dict[str, str]] = None + cla_check: dict[str, str] | None = None @dataclass @@ -139,28 +141,29 @@ class PrDesiredInfo: """ The information we want to have for a pull request. """ + # Is this an "external" pull request (True), or internal (False)? is_ospr: bool = False # Is this pull request being refused? is_refused: bool = False - bot_comments: Set[BotComment] = field(default_factory=set) - bot_comments_to_remove: Set[BotComment] = field(default_factory=set) - jira_title: Optional[str] = None - jira_description: Optional[str] = None + bot_comments: set[BotComment] = field(default_factory=set) + bot_comments_to_remove: set[BotComment] = field(default_factory=set) + jira_title: str | None = None + jira_description: str | None = None # The Jira instances we want to have issues on. - jira_nicks: Set[str] = field(default_factory=set) + jira_nicks: set[str] = field(default_factory=set) # The bot-controlled labels we want to on the pull request. # See labels.py:CATEGORY_LABELS - github_labels: Set[str] = field(default_factory=set) + github_labels: set[str] = field(default_factory=set) # The GitHub projects we want the PR in. - github_projects: Set[GhProject] = field(default_factory=set) + github_projects: set[GhProject] = field(default_factory=set) # The status of the cla check. - cla_check: Optional[Dict[str, str]] = None + cla_check: dict[str, str] | None = None @dataclass @@ -168,10 +171,11 @@ class FixResult: """ Return value from PrTrackingFixer.result. """ + # The Jira issues associated with the pull request. - jira_issues: Set[JiraId] = field(default_factory=set) + jira_issues: set[JiraId] = field(default_factory=set) # The Jira issues that were created or changed. - changed_jira_issues: Set[JiraId] = field(default_factory=set) + changed_jira_issues: set[JiraId] = field(default_factory=set) def current_support_state(pr: PrDict) -> PrCurrentInfo: @@ -194,7 +198,7 @@ def current_support_state(pr: PrDict) -> PrCurrentInfo: current.bot_survey_comment_id = comment["id"] current.bot_data.update(extract_data_from_comment(body)) - current.github_labels = set(lbl["name"] for lbl in pr["labels"]) + current.github_labels = {lbl["name"] for lbl in pr["labels"]} current.github_projects_info = pull_request_projects_info(pr) current.github_projects = pull_request_projects(pr, current.github_projects_info) current.cla_check = cla_status_on_pr(pr) @@ -211,7 +215,7 @@ def desired_support_state(pr: PrDict) -> PrDesiredInfo: user = pr["user"]["login"] repo = pr["base"]["repo"]["full_name"] num = pr["number"] - label_names = set(lbl["name"] for lbl in pr["labels"]) + label_names = {lbl["name"] for lbl in pr["labels"]} user_is_bot = is_bot_pull_request(pr) no_cla_is_needed = is_private_repo_no_cla_pull_request(pr) @@ -244,13 +248,11 @@ def desired_support_state(pr: PrDict) -> PrDesiredInfo: raise Exception(f"A crash label was applied by {user}") desired.jira_title = pr["title"] - desired.jira_description = ( - "(From {url} by {user_url})\n------\n\n{body}" - ).format( - url=pr["html_url"], - body=(pr["body"] or ""), - user_url=pr["user"]["html_url"], - ) + desired.jira_description = ("(From {url} by {user_url})\n------\n\n{body}").format( + url=pr["html_url"], + body=(pr["body"] or ""), + user_url=pr["user"]["html_url"], + ) blended_id = get_blended_project_id(pr) if blended_id is not None: @@ -298,10 +300,10 @@ def desired_support_state(pr: PrDict) -> PrDesiredInfo: if state == "reopened": desired.bot_comments_to_remove.add(BotComment.SURVEY) -# # temp: Disable survey link on pull requests -# # https://github.com/openedx/openedx-webhooks/issues/259 -# if state in ["closed", "merged"]: -# desired.bot_comments.add(BotComment.SURVEY) + # # temp: Disable survey link on pull requests + # # https://github.com/openedx/openedx-webhooks/issues/259 + # if state in ["closed", "merged"]: + # desired.bot_comments.add(BotComment.SURVEY) if desired.is_refused and state not in ["closed", "merged"]: desired.bot_comments.add(BotComment.NO_CONTRIBUTIONS) @@ -313,7 +315,7 @@ def json_safe_dict(dc): """ Make a JSON-safe dict from a dataclass, for recording info during dry runs. """ - return {k:repr(v) for k, v in dataclasses.asdict(dc).items()} + return {k: repr(v) for k, v in dataclasses.asdict(dc).items()} class PrTrackingFixer: @@ -336,7 +338,7 @@ def __init__( self.bot_data = copy.deepcopy(current.bot_data) self.fix_result: FixResult = FixResult() - self.exceptions: List[Exception] = [] + self.exceptions: list[Exception] = [] def result(self) -> FixResult: return self.fix_result @@ -350,7 +352,7 @@ def saved_exceptions(self): """ try: yield - except Exception as exc: # pylint: disable=broad-exception-caught + except Exception as exc: # pylint: disable=broad-exception-caught self.exceptions.append(exc) def fix(self) -> None: @@ -418,20 +420,22 @@ def _fix_projects(self) -> None: """ Update projects for pr. """ - for project in (self.desired.github_projects - self.current.github_projects): - project_item_id = self.actions.add_pull_request_to_project( - pr_node_id=self.pr["node_id"], project=project + for project in self.desired.github_projects - self.current.github_projects: + project_item_id = self.actions.add_pull_request_to_project(pr_node_id=self.pr["node_id"], project=project) + self.current.github_projects_info.append( + { + "id": project_item_id, + "org": project[0], + "number": project[1], + } ) - self.current.github_projects_info.append({ - "id": project_item_id, - "org": project[0], - "number": project[1], - }) def _fix_project_node_fields(self) -> None: """ Update pr fields in OSPR project board. """ + if settings.GITHUB_OSPR_PROJECT is None: + return for project in self.current.github_projects_info: if ( project["org"] == settings.GITHUB_OSPR_PROJECT[0] @@ -447,7 +451,7 @@ def _fix_project_node_fields(self) -> None: field_name="Date opened", field_value=self.pr["created_at"], item_id=project_item_id, - project=settings.GITHUB_OSPR_PROJECT + project=settings.GITHUB_OSPR_PROJECT, ) # get base repo owner info repo_spec = get_repo_spec(self.pr["base"]["repo"]["full_name"]) @@ -463,7 +467,7 @@ def _fix_project_node_fields(self) -> None: field_name="Repo Owner / Owning Team", field_value=owner, item_id=project_item_id, - project=settings.GITHUB_OSPR_PROJECT + project=settings.GITHUB_OSPR_PROJECT, ) elif state == "merged": merged_at = self.pr.get("merged_at") @@ -480,14 +484,14 @@ def _fix_project_node_fields(self) -> None: field_name="Date merged/closed", field_value=merged_at, item_id=project_item_id, - project=settings.GITHUB_OSPR_PROJECT + project=settings.GITHUB_OSPR_PROJECT, ) elif state == "closed": self.actions.update_project_pr_custom_field( field_name="Date merged/closed", field_value=self.pr["closed_at"], item_id=project_item_id, - project=settings.GITHUB_OSPR_PROJECT + project=settings.GITHUB_OSPR_PROJECT, ) def _make_jira_issue(self, jira_nick) -> None: @@ -584,9 +588,7 @@ def _fix_bot_comment(self) -> None: # No body, no comment to make. return - comment_body += format_data_for_comment({ - "draft": is_draft_pull_request(self.pr) - }) + comment_body += format_data_for_comment({"draft": is_draft_pull_request(self.pr)}) if comment_body != self.current.bot_comment0_text: # If there are current-state comments, then we need to edit the @@ -624,6 +626,7 @@ class DryRunFixingActions: """ Implementation of actions for dry runs. """ + jira_ids = itertools.count(start=9000) def __init__(self): @@ -644,6 +647,7 @@ def create_jira_issue(self, **kwargs): def __getattr__(self, name): def fn(**kwargs): self.action_calls.append((name, kwargs)) + return fn @@ -660,20 +664,21 @@ class FixingActions: def __init__(self, prid: PrId): self.prid = prid - def initial_state(self, *, current: Dict, desired: Dict) -> None: + def initial_state(self, *, current: dict, desired: dict) -> None: """ Does nothing when really fixing, but captures information for dry runs. """ def create_jira_issue( - self, *, + self, + *, jira_nick: str, project: str, issuetype: str, - summary: Optional[str], - description: Optional[str], - labels: List[str], - ) -> Dict: + summary: str | None, + description: str | None, + labels: list[str], + ) -> dict: """ Create a new Jira issue for a pull request. @@ -736,7 +741,7 @@ def delete_comment_on_pull_request(self, *, comment_id: str) -> None: resp = get_github_session().delete(url) log_check_response(resp) - def update_labels_on_pull_request(self, *, labels: List[str]) -> None: + def update_labels_on_pull_request(self, *, labels: list[str]) -> None: """ Change the labels on a pull request. @@ -754,7 +759,7 @@ def add_pull_request_to_project(self, *, pr_node_id: str, project: GhProject) -> """ try: return add_pull_request_to_project(self.prid, pr_node_id, project) - except Exception: # pylint: disable=broad-exception-caught + except Exception: # pylint: disable=broad-exception-caught logger.exception("Couldn't add PR to project") return None @@ -764,8 +769,8 @@ def update_project_pr_custom_field(self, *, field_name: str, field_value, item_i """ try: update_project_pr_custom_field(field_name, field_value, item_id, project) - except Exception: # pylint: disable=broad-exception-caught + except Exception: # pylint: disable=broad-exception-caught logger.exception(f"Couldn't update: {field_name} for a PR in project") - def set_cla_status(self, *, status: Dict[str, str]) -> None: + def set_cla_status(self, *, status: dict[str, str]) -> None: set_cla_status_on_pr(self.prid.full_name, self.prid.number, status) diff --git a/openedx_webhooks/types.py b/openedx_webhooks/types.py index 005ce695..29072be2 100644 --- a/openedx_webhooks/types.py +++ b/openedx_webhooks/types.py @@ -3,30 +3,30 @@ from __future__ import annotations import dataclasses -from typing import Dict, Tuple # A pull request as described by a JSON object. -PrDict = Dict +PrDict = dict # A pull request comment as described by a JSON object. -PrCommentDict = Dict +PrCommentDict = dict # A Jira issue described by a JSON object. -JiraDict = Dict +JiraDict = dict # A GitHub project: org name, and number. -GhProject = Tuple[str, int] +GhProject = tuple[str, int] # A GitHub project info: org name, number and pr node id in project. -PrGhProject = Dict +PrGhProject = dict # A GitHub project metadata json object. -GhPrMetaDict = Dict +GhPrMetaDict = dict @dataclasses.dataclass(frozen=True) class PrId: """An id of a pull request, with a repo full_name and an id.""" + full_name: str number: int @@ -46,6 +46,7 @@ def org(self): @dataclasses.dataclass(frozen=True) class JiraServer: """A Jira server and its credentials.""" + # The URL of the Jira server. server: str @@ -69,6 +70,7 @@ class JiraServer: @dataclasses.dataclass(frozen=True) class JiraId: """A JiraServer nickname and an issue key.""" + nick: str key: str diff --git a/openedx_webhooks/ui.py b/openedx_webhooks/ui.py index 02d00b47..48a1348a 100644 --- a/openedx_webhooks/ui.py +++ b/openedx_webhooks/ui.py @@ -5,9 +5,10 @@ from openedx_webhooks.auth import get_github_session from openedx_webhooks.utils import requires_auth -ui = Blueprint('ui', __name__) +ui = Blueprint("ui", __name__) logger = logging.getLogger(__name__) + @ui.route("/") @requires_auth def index(): @@ -21,7 +22,7 @@ def index(): try: github_username = gh_user_resp.json()["login"] except Exception: - logger.error("Failed to process response: {}".format(gh_user_resp.text)) + logger.error(f"Failed to process response: {gh_user_resp.text}") raise return render_template("main.html", github_username=github_username) diff --git a/openedx_webhooks/utils.py b/openedx_webhooks/utils.py index 5bee2883..a7ef573d 100644 --- a/openedx_webhooks/utils.py +++ b/openedx_webhooks/utils.py @@ -9,12 +9,12 @@ import time from functools import wraps from hashlib import sha1 -from time import sleep as retry_sleep # so that we can patch it for tests. -from typing import Dict, Optional +from time import sleep as retry_sleep # so that we can patch it for tests. +from typing import Any import cachetools.func import requests -from flask import jsonify, request, Response, url_for +from flask import Response, jsonify, request, url_for from urlobject import URLObject from openedx_webhooks import logger @@ -36,21 +36,22 @@ def _check_auth(username, password): """ Checks if a username / password combination is valid. """ - return ( - username == os.environ.get('HTTP_BASIC_AUTH_USERNAME') and - password == os.environ.get('HTTP_BASIC_AUTH_PASSWORD') + return username == os.environ.get("HTTP_BASIC_AUTH_USERNAME") and password == os.environ.get( + "HTTP_BASIC_AUTH_PASSWORD" ) + def _authenticate(): """ Sends a 401 response that enables basic auth """ return Response( - 'Could not verify your access level for that URL.\n' - 'You have to login with proper credentials', 401, - {'WWW-Authenticate': 'Basic realm="Login Required"'} + "Could not verify your access level for that URL.\nYou have to login with proper credentials", + 401, + {"WWW-Authenticate": 'Basic realm="Login Required"'}, ) + def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): @@ -58,12 +59,14 @@ def decorated(*args, **kwargs): if not auth or not _check_auth(auth.username, auth.password): return _authenticate() return f(*args, **kwargs) + return decorated class RequestFailed(Exception): pass + def log_check_response(response, raise_for_status=True): """ Logs HTTP request and response at debug level and checks if it succeeded. @@ -73,22 +76,24 @@ def log_check_response(response, raise_for_status=True): raise_for_status (bool): if True, call raise_for_status on the response also. """ - msg = "Request: {0.method} {0.url}: {0.body!r}".format(response.request) + msg = f"Request: {response.request.method} {response.request.url}: {response.request.body!r}" logger.debug(msg) - msg = "Response: {0.status_code} {0.reason!r} for {0.url}: {0.content!r}".format(response) + msg = f"Response: {response.status_code} {response.reason!r} for {response.url}: {response.content!r}" logger.debug(msg) if raise_for_status: try: response.raise_for_status() except Exception as exc: req = response.request - raise RequestFailed(f"HTTP request failed: {req.method} {req.url}. Response body: {response.content}") from exc + raise RequestFailed( + f"HTTP request failed: {req.method} {req.url}. Response body: {response.content}" + ) from exc def log_rate_limit(): """Get stats from GitHub about the current rate limit, and log them.""" - rate = get_github_session().get("/rate_limit").json()['rate'] - reset = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(rate['reset'])) + rate = get_github_session().get("/rate_limit").json()["rate"] + reset = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(rate["reset"])) logger.info(f"Rate limit: {rate['limit']}, used {rate['used']}, remaining {rate['remaining']}. Reset is at {reset}") @@ -109,7 +114,7 @@ def is_valid_payload(secret: str, signature: str, payload: bytes) -> bool: bool: Is the payload legit? """ mac = hmac.new(secret.encode(), msg=payload, digestmod=sha1) - digest = 'sha1=' + mac.hexdigest() + digest = "sha1=" + mac.hexdigest() return hmac.compare_digest(digest.encode(), signature.encode()) @@ -143,7 +148,7 @@ def retry_get(session, url, **kwargs): tries -= 1 if tries == 0: break - retry_sleep(.5) + retry_sleep(0.5) continue else: break @@ -163,7 +168,7 @@ def paginated_get(url, session=None, limit=None, per_page=100, callback=None, ** limit of 250, three requests will be made, and you'll get 300 objects. """ - url = URLObject(url).set_query_param('per_page', str(per_page)) + url = URLObject(url).set_query_param("per_page", str(per_page)) limit = limit or 999999999 session = session or requests.Session() returned = 0 @@ -180,9 +185,9 @@ def paginated_get(url, session=None, limit=None, per_page=100, callback=None, ** url = resp.links.get("next", {}).get("url", "") -def jira_paginated_get(url, session=None, - start=0, start_param="startAt", obj_name=None, - retries=3, debug=False, **fields): +def jira_paginated_get( + url, session=None, start=0, start_param="startAt", obj_name=None, retries=3, debug=False, **fields +): """ Like ``paginated_get``, but uses JIRA's conventions for a paginated API, which are different from Github's conventions. @@ -191,10 +196,7 @@ def jira_paginated_get(url, session=None, url = URLObject(url) more_results = True while more_results: - result_url = ( - url.set_query_param(start_param, str(start)) - .set_query_params(**fields) - ) + result_url = url.set_query_param(start_param, str(start)).set_query_params(**fields) for _ in range(retries): try: if debug: @@ -212,8 +214,7 @@ def jira_paginated_get(url, session=None, objs = result[obj_name] else: objs = result - for obj in objs: - yield obj + yield from objs # are we done yet? if isinstance(result, dict): returned = len(objs) @@ -237,14 +238,14 @@ def value_graphql_type(field_type: str) -> str: return "String" -def graphql_query(query: str, variables: Dict = {}) -> Dict: # pylint: disable=dangerous-default-value +def graphql_query(query: str, variables: dict | None = None) -> dict: """ Make a GraphQL query against GitHub. """ url = "https://api.github.com/graphql" body = { "query": query, - "variables": variables, + "variables": variables or {}, } response = get_github_session().post(url, json=body) log_check_response(response) @@ -256,7 +257,8 @@ def graphql_query(query: str, variables: Dict = {}) -> Dict: # pylint: disabl # A list of all the memoized functions, so that `clear_memoized_values` can # clear them all. -_memoized_functions = [] +_memoized_functions: list[Any] = [] + def memoize(func): """Cache the value returned by a function call forever.""" @@ -264,19 +266,24 @@ def memoize(func): _memoized_functions.append(func) return func + def memoize_timed(minutes): """Cache the value of a function for `minutes` minutes.""" + def _timed(func): # We use time.time as the timer so that freezegun can test it, and in a # new function so that freezegun's patching will work. Freezegun doesn't # patch time.monotonic, and we aren't that picky about the time anyway. def patchable_timer(): return time.time() + func = cachetools.func.ttl_cache(ttl=60 * minutes, timer=patchable_timer)(func) _memoized_functions.append(func) return func + return _timed + def clear_memoized_values(): """Clear all the values saved by @memoize and @memoize_timed, to ensure isolated tests.""" for func in _memoized_functions: @@ -285,11 +292,16 @@ def clear_memoized_values(): def minimal_wsgi_environ(): values = { - "HTTP_HOST", "SERVER_NAME", "SERVER_PORT", "REQUEST_METHOD", - "SCRIPT_NAME", "PATH_INFO", "QUERY_STRING", "wsgi.url_scheme", + "HTTP_HOST", + "SERVER_NAME", + "SERVER_PORT", + "REQUEST_METHOD", + "SCRIPT_NAME", + "PATH_INFO", + "QUERY_STRING", + "wsgi.url_scheme", } - return {key: value for key, value in request.environ.items() - if key in values} + return {key: value for key, value in request.environ.items() if key in values} def queue_task(task, *args, **kwargs): @@ -310,12 +322,13 @@ def queue_task(task, *args, **kwargs): def sentry_extra_context(data_dict): """Apply the keys and values from data_dict to the Sentry extra context.""" from sentry_sdk import configure_scope + with configure_scope() as scope: for key, value in data_dict.items(): scope.set_extra(key, value) -def get_jira_issue(jira_nick: str, key: str, missing_ok: bool = False) -> Optional[JiraDict]: +def get_jira_issue(jira_nick: str, key: str, missing_ok: bool = False) -> JiraDict | None: """ Get the dictionary for a Jira issue, from its key. @@ -328,7 +341,7 @@ def get_jira_issue(jira_nick: str, key: str, missing_ok: bool = False) -> Option is missing. """ - resp = jira_get(jira_nick, "/rest/api/2/issue/{key}".format(key=key)) + resp = jira_get(jira_nick, f"/rest/api/2/issue/{key}") if resp.status_code == 404 and missing_ok: return None log_check_response(resp) diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 15144914..00000000 --- a/pylintrc +++ /dev/null @@ -1,386 +0,0 @@ -# *************************** -# ** DO NOT EDIT THIS FILE ** -# *************************** -# -# This file was generated by edx-lint: https://github.com/openedx/edx-lint -# -# If you want to change this file, you have two choices, depending on whether -# you want to make a local change that applies only to this repo, or whether -# you want to make a central change that applies to all repos using edx-lint. -# -# Note: If your pylintrc file is simply out-of-date relative to the latest -# pylintrc in edx-lint, ensure you have the latest edx-lint installed -# and then follow the steps for a "LOCAL CHANGE". -# -# LOCAL CHANGE: -# -# 1. Edit the local pylintrc_tweaks file to add changes just to this -# repo's file. -# -# 2. Run: -# -# $ edx_lint write pylintrc -# -# 3. This will modify the local file. Submit a pull request to get it -# checked in so that others will benefit. -# -# -# CENTRAL CHANGE: -# -# 1. Edit the pylintrc file in the edx-lint repo at -# https://github.com/openedx/edx-lint/blob/master/edx_lint/files/pylintrc -# -# 2. install the updated version of edx-lint (in edx-lint): -# -# $ pip install . -# -# 3. Run (in edx-lint): -# -# $ edx_lint write pylintrc -# -# 4. Make a new version of edx_lint, submit and review a pull request with the -# pylintrc update, and after merging, update the edx-lint version and -# publish the new version. -# -# 5. In your local repo, install the newer version of edx-lint. -# -# 6. Run: -# -# $ edx_lint write pylintrc -# -# 7. This will modify the local file. Submit a pull request to get it -# checked in so that others will benefit. -# -# -# -# -# -# STAY AWAY FROM THIS FILE! -# -# -# -# -# -# SERIOUSLY. -# -# ------------------------------ -# Generated by edx-lint version: 5.3.4 -# ------------------------------ -[MASTER] -ignore = -persistent = yes -load-plugins = edx_lint.pylint,pylint_celery,pylint_pytest - -[MESSAGES CONTROL] -enable = - blacklisted-name, - line-too-long, - - abstract-class-instantiated, - abstract-method, - access-member-before-definition, - anomalous-backslash-in-string, - anomalous-unicode-escape-in-string, - arguments-differ, - assert-on-tuple, - assigning-non-slot, - assignment-from-no-return, - assignment-from-none, - attribute-defined-outside-init, - bad-except-order, - bad-format-character, - bad-format-string-key, - bad-format-string, - bad-open-mode, - bad-reversed-sequence, - bad-staticmethod-argument, - bad-str-strip-call, - bad-super-call, - binary-op-exception, - boolean-datetime, - catching-non-exception, - cell-var-from-loop, - confusing-with-statement, - continue-in-finally, - dangerous-default-value, - duplicate-argument-name, - duplicate-bases, - duplicate-except, - duplicate-key, - expression-not-assigned, - format-combined-specification, - format-needs-mapping, - function-redefined, - global-variable-undefined, - import-error, - import-self, - inconsistent-mro, - inherit-non-class, - init-is-generator, - invalid-all-object, - invalid-format-index, - invalid-length-returned, - invalid-sequence-index, - invalid-slice-index, - invalid-slots-object, - invalid-slots, - invalid-unary-operand-type, - logging-too-few-args, - logging-too-many-args, - logging-unsupported-format, - lost-exception, - method-hidden, - misplaced-bare-raise, - misplaced-future, - missing-format-argument-key, - missing-format-attribute, - missing-format-string-key, - no-member, - no-method-argument, - no-name-in-module, - no-self-argument, - no-value-for-parameter, - non-iterator-returned, - non-parent-method-called, - nonexistent-operator, - not-a-mapping, - not-an-iterable, - not-callable, - not-context-manager, - not-in-loop, - pointless-statement, - pointless-string-statement, - raising-bad-type, - raising-non-exception, - redefined-builtin, - redefined-outer-name, - redundant-keyword-arg, - repeated-keyword, - return-arg-in-generator, - return-in-init, - return-outside-function, - signature-differs, - super-init-not-called, - super-method-not-called, - syntax-error, - test-inherits-tests, - too-few-format-args, - too-many-format-args, - too-many-function-args, - translation-of-non-string, - truncated-format-string, - undefined-all-variable, - undefined-loop-variable, - undefined-variable, - unexpected-keyword-arg, - unexpected-special-method-signature, - unpacking-non-sequence, - unreachable, - unsubscriptable-object, - unsupported-binary-operation, - unsupported-membership-test, - unused-format-string-argument, - unused-format-string-key, - used-before-assignment, - using-constant-test, - yield-outside-function, - - astroid-error, - fatal, - method-check-failed, - parse-error, - raw-checker-failed, - - empty-docstring, - invalid-characters-in-docstring, - missing-docstring, - wrong-spelling-in-comment, - wrong-spelling-in-docstring, - - unused-argument, - unused-import, - unused-variable, - - eval-used, - exec-used, - - bad-classmethod-argument, - bad-mcs-classmethod-argument, - bad-mcs-method-argument, - bare-except, - broad-except, - consider-iterating-dictionary, - consider-using-enumerate, - global-at-module-level, - global-variable-not-assigned, - literal-used-as-attribute, - logging-format-interpolation, - logging-not-lazy, - multiple-imports, - multiple-statements, - no-classmethod-decorator, - no-staticmethod-decorator, - protected-access, - redundant-unittest-assert, - reimported, - simplifiable-if-statement, - simplifiable-range, - singleton-comparison, - superfluous-parens, - unidiomatic-typecheck, - unnecessary-lambda, - unnecessary-pass, - unnecessary-semicolon, - unneeded-not, - useless-else-on-loop, - wrong-assert-type, - - deprecated-method, - deprecated-module, - - too-many-boolean-expressions, - too-many-nested-blocks, - too-many-statements, - - wildcard-import, - wrong-import-order, - wrong-import-position, - - missing-final-newline, - mixed-line-endings, - trailing-newlines, - trailing-whitespace, - unexpected-line-ending-format, - - bad-inline-option, - bad-option-value, - deprecated-pragma, - unrecognized-inline-option, - useless-suppression, -disable = - bad-indentation, - broad-exception-raised, - consider-using-f-string, - duplicate-code, - file-ignored, - fixme, - global-statement, - invalid-name, - locally-disabled, - no-else-return, - suppressed-message, - too-few-public-methods, - too-many-ancestors, - too-many-arguments, - too-many-branches, - too-many-instance-attributes, - too-many-lines, - too-many-locals, - too-many-public-methods, - too-many-return-statements, - ungrouped-imports, - unspecified-encoding, - unused-wildcard-import, - use-maxsplit-arg, - - feature-toggle-needs-doc, - illegal-waffle-usage, - - logging-fstring-interpolation, - -[REPORTS] -output-format = text -reports = no -score = no - -[BASIC] -module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ -const-rgx = (([A-Z_][A-Z0-9_]*)|(__.*__)|log|urlpatterns)$ -class-rgx = [A-Z_][a-zA-Z0-9]+$ -function-rgx = ([a-z_][a-z0-9_]{2,40}|test_[a-z0-9_]+)$ -method-rgx = ([a-z_][a-z0-9_]{2,40}|setUp|set[Uu]pClass|tearDown|tear[Dd]ownClass|assert[A-Z]\w*|maxDiff|test_[a-z0-9_]+)$ -attr-rgx = [a-z_][a-z0-9_]{2,30}$ -argument-rgx = [a-z_][a-z0-9_]{2,30}$ -variable-rgx = [a-z_][a-z0-9_]{2,30}$ -class-attribute-rgx = ([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ -inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$ -good-names = f,i,j,k,db,ex,Run,_,__ -bad-names = foo,bar,baz,toto,tutu,tata -no-docstring-rgx = __.*__$|test_.+|setUp$|setUpClass$|tearDown$|tearDownClass$|Meta$ -docstring-min-length = 5 - -[FORMAT] -max-line-length = 120 -ignore-long-lines = ^\s*(# )?((?)|(\.\. \w+: .*))$ -single-line-if-stmt = no -max-module-lines = 1000 -indent-string = ' ' - -[MISCELLANEOUS] -notes = FIXME,XXX,TODO - -[SIMILARITIES] -min-similarity-lines = 4 -ignore-comments = yes -ignore-docstrings = yes -ignore-imports = no - -[TYPECHECK] -ignore-mixin-members = yes -ignored-classes = SQLObject -unsafe-load-any-extension = yes -generated-members = - REQUEST, - acl_users, - aq_parent, - objects, - DoesNotExist, - can_read, - can_write, - get_url, - size, - content, - status_code, - create, - build, - fields, - tag, - org, - course, - category, - name, - revision, - _meta, - -[VARIABLES] -init-import = no -dummy-variables-rgx = _|dummy|unused|.*_unused -additional-builtins = - -[CLASSES] -defining-attr-methods = __init__,__new__,setUp -valid-classmethod-first-arg = cls -valid-metaclass-classmethod-first-arg = mcs - -[DESIGN] -max-args = 5 -ignored-argument-names = _.* -max-locals = 15 -max-returns = 6 -max-branches = 12 -max-statements = 50 -max-parents = 7 -max-attributes = 7 -min-public-methods = 2 -max-public-methods = 20 - -[IMPORTS] -deprecated-modules = regsub,TERMIOS,Bastion,rexec -import-graph = -ext-import-graph = -int-import-graph = - -[EXCEPTIONS] -overgeneral-exceptions = builtins.Exception - -# f9fb9a3189dea137f974048ba886a47f1211eb65 diff --git a/pylintrc_tweaks b/pylintrc_tweaks deleted file mode 100644 index f22db427..00000000 --- a/pylintrc_tweaks +++ /dev/null @@ -1,2 +0,0 @@ -[MASTER] -load-plugins = edx_lint.pylint,pylint_celery,pylint_pytest diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c9a66ed7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,194 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "openedx-webhooks" +version = "0.1.0" +description = "Automated tasks for Open edX" +readme = "README.rst" +requires-python = ">=3.12" +license = "Apache-2.0" +authors = [ + {name = "Open edX Project", email = "oscm@openedx.org"}, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Framework :: Flask", + "Intended Audience :: Developers", + "Natural Language :: English", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] +keywords = [ + "Python", + "edx", + "openedx", + "webhooks", +] +dependencies = [ + "Flask", + "Flask-SSLify", + "PyYAML", + "URLObject", + "arrow", + "blinker", + "cachetools", + "celery[redis]", + "click", + "cryptography", + "github3.py", + "glom", + "gunicorn", + "jira", + "logging_tree", + "oauthlib[signedtoken]", + "redis", + "requests", + "requests-oauthlib", + "sentry-sdk[flask]", +] + +[project.urls] +Repository = "https://github.com/openedx/openedx-webhooks" + +[tool.setuptools.packages.find] +exclude = ["tests*", "*.tests", "*.tests.*", "docs*"] + +[dependency-groups] +# Base test packages +test-base = [ + "freezegun", + "pytest", + "pytest-cov", + "pytest-mock", + "pytest-repeat", + "pytz", + "requests-mock", + "repo-tools-data-schema @ git+https://github.com/openedx/repo-tools-data-schema.git", +] +# Default test group +test = [ + {include-group = "test-base"}, +] +quality = [ + "mypy", + "ruff", + "types-PyYAML", + "types-cachetools", + "types-requests", +] +doc = [ + {include-group = "test"}, + "Sphinx", + "readme_renderer", + "sphinx_rtd_theme", + "sphinxcontrib-httpdomain", +] +ci = [ + "tox", + "tox-uv", +] +dev = [ + {include-group = "test"}, + {include-group = "quality"}, + {include-group = "doc"}, + "edx-lint", + "python-dotenv", + "scriv", +] + +# edx-lint uv constraints configuration +# Add any repo-specific version overrides here; run `edx_lint write_uv_constraints` to apply. +[tool.edx_lint] +uv_constraints = [] + +# uv configuration +# https://docs.astral.sh/uv/reference/settings/ +# DO NOT EDIT constraint-dependencies DIRECTLY. +# This list is managed by `edx_lint write_uv_constraints` +# and will be overwritten the next time `make upgrade` is run. +# - GLOBAL constraints: edit edx_lint/files/common_constraints.txt +# - REPO-SPECIFIC constraints: edit [tool.edx_lint].uv_constraints in this file +[tool.uv] +package = true +constraint-dependencies = [ + "Django<6.0", + "elasticsearch<7.14.0", +] + +# Ruff configuration +# https://docs.astral.sh/ruff/configuration/ +[tool.ruff] +line-length = 120 +target-version = "py312" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade + "DJ", # flake8-django +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.ruff.lint.isort] +known-third-party = ["flask", "celery", "github3", "jira", "redis", "requests", "cryptography", "click", "arrow", "blinker", "cachetools", "glom", "oauthlib", "sentry_sdk"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" + +# Pytest configuration +# https://docs.pytest.org/en/stable/reference/customize.html +[tool.pytest.ini_options] +markers = [ + "flaky_github: tests to run with flaky GitHub behavior emulated", +] +filterwarnings = [ + "ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning", +] + +# Coverage configuration +# https://coverage.readthedocs.io/en/latest/config.html +[tool.coverage.run] +branch = true +source = ["openedx_webhooks"] +omit = [ + "*/tests/*", + "*/__pycache__/*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] +show_missing = true + +[tool.coverage.html] +directory = "htmlcov" + +# Mypy configuration +# https://mypy.readthedocs.io/en/stable/config_file.html +[tool.mypy] +ignore_missing_imports = true +check_untyped_defs = true + +# Scriv configuration +# https://scriv.readthedocs.io/en/latest/configuration.html +[tool.scriv] +output_file = "README.rst" +rst_header_chars = "~." + diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5603c377..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --r requirements/base.txt diff --git a/requirements/base.in b/requirements/base.in deleted file mode 100644 index 28773a69..00000000 --- a/requirements/base.in +++ /dev/null @@ -1,25 +0,0 @@ -# Core requirements for using this application - --c constraints.txt - -Flask -celery[redis] -github3.py - -Flask-SSLify -PyYAML -URLObject -arrow -blinker -cachetools -click -cryptography -glom -gunicorn -jira -logging_tree -oauthlib[signedtoken] -redis -requests -requests-oauthlib -sentry-sdk[flask] diff --git a/requirements/base.txt b/requirements/base.txt deleted file mode 100644 index b60b004b..00000000 --- a/requirements/base.txt +++ /dev/null @@ -1,159 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -amqp==5.3.1 - # via kombu -arrow==1.4.0 - # via -r requirements/base.in -attrs==26.1.0 - # via glom -billiard==4.2.4 - # via celery -blinker==1.9.0 - # via - # -r requirements/base.in - # flask - # sentry-sdk -boltons==26.0.0 - # via - # face - # glom -cachetools==7.1.4 - # via -r requirements/base.in -celery[redis]==5.6.3 - # via -r requirements/base.in -certifi==2026.6.17 - # via - # requests - # sentry-sdk -cffi==2.0.0 - # via cryptography -charset-normalizer==3.4.7 - # via requests -click==8.4.1 - # via - # -r requirements/base.in - # celery - # click-didyoumean - # click-plugins - # click-repl - # flask -click-didyoumean==0.3.1 - # via celery -click-plugins==1.1.1.2 - # via celery -click-repl==0.3.0 - # via celery -cryptography==49.0.0 - # via - # -r requirements/base.in - # oauthlib - # pyjwt -defusedxml==0.7.1 - # via jira -face==26.0.1 - # via glom -flask==3.1.3 - # via - # -r requirements/base.in - # flask-sslify - # sentry-sdk -flask-sslify==0.1.5 - # via -r requirements/base.in -github3-py==4.0.1 - # via -r requirements/base.in -glom==25.12.0 - # via -r requirements/base.in -gunicorn==26.0.0 - # via -r requirements/base.in -idna==3.18 - # via requests -itsdangerous==2.2.0 - # via flask -jinja2==3.1.6 - # via flask -jira==3.10.5 - # via -r requirements/base.in -kombu[redis]==5.6.2 - # via celery -logging-tree==1.10 - # via -r requirements/base.in -markupsafe==3.0.3 - # via - # flask - # jinja2 - # sentry-sdk - # werkzeug -oauthlib[signedtoken]==3.3.1 - # via - # -r requirements/base.in - # requests-oauthlib -packaging==26.2 - # via - # gunicorn - # jira - # kombu -prompt-toolkit==3.0.52 - # via click-repl -pycparser==3.0 - # via cffi -pyjwt[crypto]==2.13.0 - # via - # github3-py - # oauthlib -python-dateutil==2.9.0.post0 - # via - # arrow - # celery - # github3-py -pyyaml==6.0.3 - # via -r requirements/base.in -redis==6.4.0 - # via - # -r requirements/base.in - # kombu -requests==2.34.2 - # via - # -r requirements/base.in - # github3-py - # jira - # requests-oauthlib - # requests-toolbelt -requests-oauthlib==2.0.0 - # via - # -r requirements/base.in - # jira -requests-toolbelt==1.0.0 - # via jira -sentry-sdk[flask]==2.63.0 - # via -r requirements/base.in -six==1.17.0 - # via python-dateutil -typing-extensions==4.15.0 - # via jira -tzdata==2026.2 - # via - # arrow - # kombu -tzlocal==5.4.3 - # via celery -uritemplate==4.2.0 - # via github3-py -urllib3==2.7.0 - # via - # requests - # sentry-sdk -urlobject==3.0.0 - # via -r requirements/base.in -vine==5.1.0 - # via - # amqp - # celery - # kombu -wcwidth==0.8.1 - # via prompt-toolkit -werkzeug==3.1.8 - # via flask diff --git a/requirements/constraints.txt b/requirements/constraints.txt deleted file mode 100644 index 75686f42..00000000 --- a/requirements/constraints.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Version constraints for pip-installation. -# -# This file doesn't install any packages. It specifies version constraints -# that will be applied if a package is needed. -# -# When pinning something here, please provide an explanation of why. Ideally, -# link to other information that will help people in the future to remove the -# pin when possible. Writing an issue against the offending project and -# linking to it here is good. - -# Common constraints for Open edX repos --c https://raw.githubusercontent.com/openedx/edx-lint/master/edx_lint/files/common_constraints.txt diff --git a/requirements/dev.in b/requirements/dev.in deleted file mode 100644 index a22f69cd..00000000 --- a/requirements/dev.in +++ /dev/null @@ -1,14 +0,0 @@ -# Additional requirements for development of this application --r base.in --r test.in - -edx-lint # For updating pylintrc -pylint-pytest # So that pylint understands pytest fixture naming -mypy -isort -pip-tools # Requirements file management -python-dotenv -scriv # Changelog management -types-PyYAML -types-cachetools -types-requests diff --git a/requirements/dev.txt b/requirements/dev.txt deleted file mode 100644 index 3aeb26ba..00000000 --- a/requirements/dev.txt +++ /dev/null @@ -1,353 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -alabaster==1.0.0 - # via sphinx -amqp==5.3.1 - # via kombu -arrow==1.4.0 - # via -r requirements/base.in -ast-serialize==0.5.0 - # via mypy -astroid==4.0.4 - # via - # pylint - # pylint-celery -attrs==26.1.0 - # via - # glom - # scriv -babel==2.18.0 - # via sphinx -billiard==4.2.4 - # via celery -blinker==1.9.0 - # via - # -r requirements/base.in - # flask - # sentry-sdk -boltons==26.0.0 - # via - # face - # glom -build==1.5.0 - # via pip-tools -cachetools==7.1.4 - # via -r requirements/base.in -celery[redis]==5.6.3 - # via -r requirements/base.in -certifi==2026.6.17 - # via - # requests - # sentry-sdk -cffi==2.0.0 - # via cryptography -charset-normalizer==3.4.7 - # via requests -click==8.4.1 - # via - # -r requirements/base.in - # celery - # click-didyoumean - # click-log - # click-plugins - # click-repl - # code-annotations - # edx-lint - # flask - # pip-tools - # scriv -click-didyoumean==0.3.1 - # via celery -click-log==0.4.0 - # via - # edx-lint - # scriv -click-plugins==1.1.1.2 - # via celery -click-repl==0.3.0 - # via celery -code-annotations==3.0.0 - # via edx-lint -coverage[toml]==7.14.2 - # via pytest-cov -cryptography==49.0.0 - # via - # -r requirements/base.in - # oauthlib - # pyjwt -defusedxml==0.7.1 - # via jira -dill==0.4.1 - # via pylint -docutils==0.22.4 - # via - # readme-renderer - # sphinx - # sphinx-rtd-theme -edx-lint==6.1.0 - # via -r requirements/dev.in -face==26.0.1 - # via glom -flask==3.1.3 - # via - # -r requirements/base.in - # flask-sslify - # sentry-sdk -flask-sslify==0.1.5 - # via -r requirements/base.in -freezegun==1.5.5 - # via -r requirements/test.in -github3-py==4.0.1 - # via -r requirements/base.in -glom==25.12.0 - # via -r requirements/base.in -gunicorn==26.0.0 - # via -r requirements/base.in -idna==3.18 - # via requests -imagesize==2.0.0 - # via sphinx -iniconfig==2.3.0 - # via pytest -isort==8.0.1 - # via - # -r requirements/dev.in - # pylint -itsdangerous==2.2.0 - # via flask -jinja2==3.1.6 - # via - # code-annotations - # flask - # scriv - # sphinx -jira==3.10.5 - # via -r requirements/base.in -kombu[redis]==5.6.2 - # via celery -librt==0.11.0 - # via mypy -logging-tree==1.10 - # via -r requirements/base.in -markdown-it-py==4.2.0 - # via scriv -markupsafe==3.0.3 - # via - # flask - # jinja2 - # sentry-sdk - # werkzeug -mccabe==0.7.0 - # via pylint -mdurl==0.1.2 - # via markdown-it-py -mypy==2.1.0 - # via -r requirements/dev.in -mypy-extensions==1.1.0 - # via mypy -nh3==0.3.5 - # via readme-renderer -oauthlib[signedtoken]==3.3.1 - # via - # -r requirements/base.in - # requests-oauthlib -packaging==26.2 - # via - # build - # gunicorn - # jira - # kombu - # pytest - # sphinx - # wheel -pathspec==1.1.1 - # via mypy -pip-tools==7.5.3 - # via -r requirements/dev.in -platformdirs==4.10.0 - # via pylint -pluggy==1.6.0 - # via - # pytest - # pytest-cov -prompt-toolkit==3.0.52 - # via click-repl -pycparser==3.0 - # via cffi -pygments==2.20.0 - # via - # readme-renderer - # sphinx -pyjwt[crypto]==2.13.0 - # via - # github3-py - # oauthlib -pylint==4.0.6 - # via - # edx-lint - # pylint-celery - # pylint-django - # pylint-plugin-utils - # pylint-pytest -pylint-celery==0.3 - # via edx-lint -pylint-django==2.7.0 - # via edx-lint -pylint-plugin-utils==0.9.0 - # via - # pylint-celery - # pylint-django -pylint-pytest==1.1.8 - # via -r requirements/dev.in -pyproject-hooks==1.2.0 - # via - # build - # pip-tools -pytest==8.2.0 - # via - # -r requirements/test.in - # pylint-pytest - # pytest-cov - # pytest-mock - # pytest-repeat -pytest-cov==7.1.0 - # via -r requirements/test.in -pytest-mock==3.15.1 - # via -r requirements/test.in -pytest-repeat==0.9.4 - # via -r requirements/test.in -python-dateutil==2.9.0.post0 - # via - # arrow - # celery - # freezegun - # github3-py -python-dotenv==1.2.2 - # via -r requirements/dev.in -python-slugify==8.0.4 - # via code-annotations -pytz==2026.2 - # via -r requirements/test.in -pyyaml==6.0.3 - # via - # -r requirements/base.in - # code-annotations - # repo-tools-data-schema -readme-renderer==45.0 - # via -r requirements/doc.in -redis==6.4.0 - # via - # -r requirements/base.in - # kombu -repo-tools-data-schema @ git+https://github.com/openedx/repo-tools-data-schema.git - # via -r requirements/test.in -requests==2.34.2 - # via - # -r requirements/base.in - # github3-py - # jira - # requests-mock - # requests-oauthlib - # requests-toolbelt - # scriv - # sphinx -requests-mock==1.12.1 - # via -r requirements/test.in -requests-oauthlib==2.0.0 - # via - # -r requirements/base.in - # jira -requests-toolbelt==1.0.0 - # via jira -roman-numerals==4.1.0 - # via sphinx -schema==0.7.8 - # via repo-tools-data-schema -scriv==1.8.0 - # via -r requirements/dev.in -sentry-sdk[flask]==2.63.0 - # via -r requirements/base.in -six==1.17.0 - # via - # edx-lint - # python-dateutil -snowballstemmer==3.1.1 - # via sphinx -sphinx==9.1.0 - # via - # -r requirements/doc.in - # sphinx-rtd-theme - # sphinxcontrib-httpdomain - # sphinxcontrib-jquery -sphinx-rtd-theme==3.1.0 - # via -r requirements/doc.in -sphinxcontrib-applehelp==2.0.0 - # via sphinx -sphinxcontrib-devhelp==2.0.0 - # via sphinx -sphinxcontrib-htmlhelp==2.1.0 - # via sphinx -sphinxcontrib-httpdomain==2.0.0 - # via -r requirements/doc.in -sphinxcontrib-jquery==4.1 - # via sphinx-rtd-theme -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-qthelp==2.0.0 - # via sphinx -sphinxcontrib-serializinghtml==2.0.0 - # via sphinx -stevedore==5.8.0 - # via code-annotations -text-unidecode==1.3 - # via python-slugify -tomlkit==0.15.0 - # via - # edx-lint - # pylint -types-cachetools==7.0.0.20260518 - # via -r requirements/dev.in -types-pyyaml==6.0.12.20260518 - # via -r requirements/dev.in -types-requests==2.33.0.20260518 - # via -r requirements/dev.in -typing-extensions==4.15.0 - # via - # jira - # mypy -tzdata==2026.2 - # via - # arrow - # kombu -tzlocal==5.4.3 - # via celery -uritemplate==4.2.0 - # via github3-py -urllib3==2.7.0 - # via - # requests - # sentry-sdk - # types-requests -urlobject==3.0.0 - # via -r requirements/base.in -vine==5.1.0 - # via - # amqp - # celery - # kombu -wcwidth==0.8.1 - # via prompt-toolkit -werkzeug==3.1.8 - # via flask -wheel==0.47.0 - # via pip-tools - -# The following packages are considered to be unsafe in a requirements file: -pip==26.1.2 - # via pip-tools -setuptools==82.0.1 - # via pip-tools diff --git a/requirements/doc.in b/requirements/doc.in deleted file mode 100644 index 029d91dc..00000000 --- a/requirements/doc.in +++ /dev/null @@ -1,9 +0,0 @@ -# Requirements for documentation validation - --c constraints.txt - -Sphinx # Documentation builder - -readme_renderer # Validates README.rst for usage on PyPI -sphinx_rtd_theme # ReadTheDocs theme for Sphinx output -sphinxcontrib-httpdomain diff --git a/requirements/doc.txt b/requirements/doc.txt deleted file mode 100644 index 1a089570..00000000 --- a/requirements/doc.txt +++ /dev/null @@ -1,69 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -alabaster==1.0.0 - # via sphinx -babel==2.18.0 - # via sphinx -certifi==2026.6.17 - # via requests -charset-normalizer==3.4.7 - # via requests -docutils==0.22.4 - # via - # readme-renderer - # sphinx - # sphinx-rtd-theme -idna==3.18 - # via requests -imagesize==2.0.0 - # via sphinx -jinja2==3.1.6 - # via sphinx -markupsafe==3.0.3 - # via jinja2 -nh3==0.3.5 - # via readme-renderer -packaging==26.2 - # via sphinx -pygments==2.20.0 - # via - # readme-renderer - # sphinx -readme-renderer==45.0 - # via -r requirements/doc.in -requests==2.34.2 - # via sphinx -roman-numerals==4.1.0 - # via sphinx -snowballstemmer==3.1.1 - # via sphinx -sphinx==9.1.0 - # via - # -r requirements/doc.in - # sphinx-rtd-theme - # sphinxcontrib-httpdomain - # sphinxcontrib-jquery -sphinx-rtd-theme==3.1.0 - # via -r requirements/doc.in -sphinxcontrib-applehelp==2.0.0 - # via sphinx -sphinxcontrib-devhelp==2.0.0 - # via sphinx -sphinxcontrib-htmlhelp==2.1.0 - # via sphinx -sphinxcontrib-httpdomain==2.0.0 - # via -r requirements/doc.in -sphinxcontrib-jquery==4.1 - # via sphinx-rtd-theme -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-qthelp==2.0.0 - # via sphinx -sphinxcontrib-serializinghtml==2.0.0 - # via sphinx -urllib3==2.7.0 - # via requests diff --git a/requirements/pip-tools.in b/requirements/pip-tools.in deleted file mode 100644 index ab0898d1..00000000 --- a/requirements/pip-tools.in +++ /dev/null @@ -1,4 +0,0 @@ --c constraints.txt -# Just the dependencies to run pip-tools, mainly for the "upgrade" make target - -pip-tools # Contains pip-compile, used to generate pip requirements files diff --git a/requirements/pip-tools.txt b/requirements/pip-tools.txt deleted file mode 100644 index 37f79761..00000000 --- a/requirements/pip-tools.txt +++ /dev/null @@ -1,28 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -build==1.5.0 - # via pip-tools -click==8.4.1 - # via pip-tools -packaging==26.2 - # via - # build - # wheel -pip-tools==7.5.3 - # via -r requirements/pip-tools.in -pyproject-hooks==1.2.0 - # via - # build - # pip-tools -wheel==0.47.0 - # via pip-tools - -# The following packages are considered to be unsafe in a requirements file: -pip==26.1.2 - # via pip-tools -setuptools==82.0.1 - # via pip-tools diff --git a/requirements/pip.in b/requirements/pip.in deleted file mode 100644 index 715478cd..00000000 --- a/requirements/pip.in +++ /dev/null @@ -1,7 +0,0 @@ --c constraints.txt -# Core dependencies for installing other packages - -pip -setuptools -wheel - diff --git a/requirements/pip.txt b/requirements/pip.txt deleted file mode 100644 index f3c15550..00000000 --- a/requirements/pip.txt +++ /dev/null @@ -1,16 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -packaging==26.2 - # via wheel -wheel==0.47.0 - # via -r requirements/pip.in - -# The following packages are considered to be unsafe in a requirements file: -pip==26.1.2 - # via -r requirements/pip.in -setuptools==82.0.1 - # via -r requirements/pip.in diff --git a/requirements/private.readme b/requirements/private.readme deleted file mode 100644 index 5600a107..00000000 --- a/requirements/private.readme +++ /dev/null @@ -1,15 +0,0 @@ -# If there are any Python packages you want to keep in your virtualenv beyond -# those listed in the official requirements files, create a "private.in" file -# and list them there. Generate the corresponding "private.txt" file pinning -# all of their indirect dependencies to specific versions as follows: - -# pip-compile private.in - -# This allows you to use "pip-sync" without removing these packages: - -# pip-sync requirements/*.txt - -# "private.in" and "private.txt" aren't checked into git to avoid merge -# conflicts, and the presence of this file allows "private.*" to be -# included in scripted pip-sync usage without requiring that those files be -# created first. diff --git a/requirements/test.in b/requirements/test.in deleted file mode 100644 index 5f54020d..00000000 --- a/requirements/test.in +++ /dev/null @@ -1,14 +0,0 @@ -# Requirements for test runs. --r base.in --r doc.in - -freezegun -pytest -pytest-cov -pytest-mock -pytest-repeat -pytz -requests-mock - -# Our repo-tools-data schema checkers -git+https://github.com/openedx/repo-tools-data-schema.git diff --git a/requirements/test.txt b/requirements/test.txt deleted file mode 100644 index f6e58d3d..00000000 --- a/requirements/test.txt +++ /dev/null @@ -1,246 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.12 -# by the following command: -# -# make upgrade -# -alabaster==1.0.0 - # via sphinx -amqp==5.3.1 - # via kombu -arrow==1.4.0 - # via -r requirements/base.in -attrs==26.1.0 - # via glom -babel==2.18.0 - # via sphinx -billiard==4.2.4 - # via celery -blinker==1.9.0 - # via - # -r requirements/base.in - # flask - # sentry-sdk -boltons==26.0.0 - # via - # face - # glom -cachetools==7.1.4 - # via -r requirements/base.in -celery[redis]==5.6.3 - # via -r requirements/base.in -certifi==2026.6.17 - # via - # requests - # sentry-sdk -cffi==2.0.0 - # via cryptography -charset-normalizer==3.4.7 - # via requests -click==8.4.1 - # via - # -r requirements/base.in - # celery - # click-didyoumean - # click-plugins - # click-repl - # flask -click-didyoumean==0.3.1 - # via celery -click-plugins==1.1.1.2 - # via celery -click-repl==0.3.0 - # via celery -coverage[toml]==7.14.2 - # via pytest-cov -cryptography==49.0.0 - # via - # -r requirements/base.in - # oauthlib - # pyjwt -defusedxml==0.7.1 - # via jira -docutils==0.22.4 - # via - # readme-renderer - # sphinx - # sphinx-rtd-theme -face==26.0.1 - # via glom -flask==3.1.3 - # via - # -r requirements/base.in - # flask-sslify - # sentry-sdk -flask-sslify==0.1.5 - # via -r requirements/base.in -freezegun==1.5.5 - # via -r requirements/test.in -github3-py==4.0.1 - # via -r requirements/base.in -glom==25.12.0 - # via -r requirements/base.in -gunicorn==26.0.0 - # via -r requirements/base.in -idna==3.18 - # via requests -imagesize==2.0.0 - # via sphinx -iniconfig==2.3.0 - # via pytest -itsdangerous==2.2.0 - # via flask -jinja2==3.1.6 - # via - # flask - # sphinx -jira==3.10.5 - # via -r requirements/base.in -kombu[redis]==5.6.2 - # via celery -logging-tree==1.10 - # via -r requirements/base.in -markupsafe==3.0.3 - # via - # flask - # jinja2 - # sentry-sdk - # werkzeug -nh3==0.3.5 - # via readme-renderer -oauthlib[signedtoken]==3.3.1 - # via - # -r requirements/base.in - # requests-oauthlib -packaging==26.2 - # via - # gunicorn - # jira - # kombu - # pytest - # sphinx -pluggy==1.6.0 - # via - # pytest - # pytest-cov -prompt-toolkit==3.0.52 - # via click-repl -pycparser==3.0 - # via cffi -pygments==2.20.0 - # via - # pytest - # readme-renderer - # sphinx -pyjwt[crypto]==2.13.0 - # via - # github3-py - # oauthlib -pytest==9.1.1 - # via - # -r requirements/test.in - # pytest-cov - # pytest-mock - # pytest-repeat -pytest-cov==7.1.0 - # via -r requirements/test.in -pytest-mock==3.15.1 - # via -r requirements/test.in -pytest-repeat==0.9.4 - # via -r requirements/test.in -python-dateutil==2.9.0.post0 - # via - # arrow - # celery - # freezegun - # github3-py -pytz==2026.2 - # via -r requirements/test.in -pyyaml==6.0.3 - # via - # -r requirements/base.in - # repo-tools-data-schema -readme-renderer==45.0 - # via -r requirements/doc.in -redis==6.4.0 - # via - # -r requirements/base.in - # kombu -repo-tools-data-schema @ git+https://github.com/openedx/repo-tools-data-schema.git - # via -r requirements/test.in -requests==2.34.2 - # via - # -r requirements/base.in - # github3-py - # jira - # requests-mock - # requests-oauthlib - # requests-toolbelt - # sphinx -requests-mock==1.12.1 - # via -r requirements/test.in -requests-oauthlib==2.0.0 - # via - # -r requirements/base.in - # jira -requests-toolbelt==1.0.0 - # via jira -roman-numerals==4.1.0 - # via sphinx -schema==0.7.8 - # via repo-tools-data-schema -sentry-sdk[flask]==2.63.0 - # via -r requirements/base.in -six==1.17.0 - # via python-dateutil -snowballstemmer==3.1.1 - # via sphinx -sphinx==9.1.0 - # via - # -r requirements/doc.in - # sphinx-rtd-theme - # sphinxcontrib-httpdomain - # sphinxcontrib-jquery -sphinx-rtd-theme==3.1.0 - # via -r requirements/doc.in -sphinxcontrib-applehelp==2.0.0 - # via sphinx -sphinxcontrib-devhelp==2.0.0 - # via sphinx -sphinxcontrib-htmlhelp==2.1.0 - # via sphinx -sphinxcontrib-httpdomain==2.0.0 - # via -r requirements/doc.in -sphinxcontrib-jquery==4.1 - # via sphinx-rtd-theme -sphinxcontrib-jsmath==1.0.1 - # via sphinx -sphinxcontrib-qthelp==2.0.0 - # via sphinx -sphinxcontrib-serializinghtml==2.0.0 - # via sphinx -typing-extensions==4.15.0 - # via jira -tzdata==2026.2 - # via - # arrow - # kombu -tzlocal==5.4.3 - # via celery -uritemplate==4.2.0 - # via github3-py -urllib3==2.7.0 - # via - # requests - # sentry-sdk -urlobject==3.0.0 - # via -r requirements/base.in -vine==5.1.0 - # via - # amqp - # celery - # kombu -wcwidth==0.8.1 - # via prompt-toolkit -werkzeug==3.1.8 - # via flask diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e810e5cb..00000000 --- a/setup.cfg +++ /dev/null @@ -1,18 +0,0 @@ -[mypy] -ignore_missing_imports = True -check_untyped_defs = True - -[tool:pytest] -markers = - flaky_github: tests to run with flaky GitHub behavior emulated - -filterwarnings = - # kombu issued this because of entry_points() on Python 3.10: - # for ep in importlib_metadata.entry_points().get(namespace, []) - ignore:SelectableGroups dict interface is deprecated. Use select.:DeprecationWarning - -[scriv] -output_file = README.rst -rst_header_chars = ~. -categories = -version = diff --git a/setup.py b/setup.py deleted file mode 100644 index 0655b783..00000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Install openedx-webhooks.""" - -import re - -from setuptools import find_packages, setup - - -# UPDATED VIA SEMGREP - if you need to remove/modify this method remove this line and add a comment specifying why -def is_requirement(line): - """ - Return True if the requirement line is a package requirement. - - Returns: - bool: True if the line is not blank, a comment, - a URL, or an included file - """ - return line and line.strip() and not line.startswith(('-r', '#', '-e', 'git+', '-c')) - - -def get_requirements(path): - with open(path) as f: - lines = f.readlines() - return [line.strip() for line in lines if is_requirement(line)] - - -version = '' -with open('openedx_webhooks/__init__.py', 'r') as fd: - version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', - fd.read(), re.MULTILINE).group(1) - -if not version: - raise RuntimeError('Cannot find version information') - - -setup( - name="openedx_webhooks", - version=version, - description="Automated tasks for Open edX", - long_description=open('README.rst').read(), - author="Open Source Community Managers at edX", - author_email="oscm@edx.org", - url="https://github.com/openedx/openedx_webhooks", - packages=find_packages(), - install_requires=get_requirements("requirements.txt"), - license='Apache 2.0', - classifiers=( - 'License :: OSI Approved :: Apache Software License', - 'Framework :: Flask', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3.12', - ), - zip_safe=False, -) diff --git a/tests/conftest.py b/tests/conftest.py index e3d6a29e..a1942c11 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,6 @@ import re from pathlib import Path -from typing import Dict import pytest import requests_mock @@ -26,10 +25,12 @@ def requests_mocker(): finally: mocker.stop() + # URLs we use to grab data from GitHub. We use requests_mock to provide # canned data during tests. DATA_REGEX = re.compile(r"https://raw.githubusercontent.com/([^/]+/[^/]+)/HEAD/(.*)") + @pytest.fixture def fake_repo_data(requests_mocker): """A fixture to use local files instead of GitHub-fetched data files.""" @@ -56,13 +57,15 @@ def hard_cache_yaml_data_files(session_mocker) -> None: Read them once per test run, and re-use the data. """ real_read_yaml_data_file = openedx_webhooks.info._read_yaml_data_file - data_files: Dict[str, Dict] = {} + data_files: dict[str, dict] = {} + def new_read_yaml_data_file(filename): data = data_files.get(filename) if data is None: data = real_read_yaml_data_file(filename) data_files[filename] = data return data + session_mocker.patch("openedx_webhooks.info._read_yaml_data_file", new_read_yaml_data_file) @@ -81,6 +84,7 @@ def settings_for_tests(mocker): if name.isupper(): mocker.patch(f"openedx_webhooks.settings.{name}", value) + @pytest.fixture def fake_github(pytestconfig, mocker, requests_mocker, fake_repo_data): fraction_404 = float(pytestconfig.getoption("percent_404")) / 100.0 @@ -94,14 +98,17 @@ def fake_github(pytestconfig, mocker, requests_mocker, fake_repo_data): def fake_jira_fixture(url): """A function to make fake Jira fixtures!""" + @pytest.fixture def _fake_jira(requests_mocker, fake_repo_data): """A FakeJira for the first server configured in our jira-info.yaml.""" the_fake_jira = FakeJira(url) the_fake_jira.install_mocks(requests_mocker) return the_fake_jira + return _fake_jira + fake_jira = fake_jira_fixture("https://test.atlassian.net") fake_jira2 = fake_jira_fixture("https://test2.atlassian.net") fake_jira_another = fake_jira_fixture("https://anotherorg.atlassian.net") @@ -114,7 +121,7 @@ def configure_flask_app(): initialized properly. """ app = openedx_webhooks.create_app(config="testing") - with app.test_request_context('/', base_url="https://openedx-webhooks.herokuapp.com"): + with app.test_request_context("/", base_url="https://openedx-webhooks.herokuapp.com"): yield @@ -124,10 +131,12 @@ def reset_all_memoized_functions(): openedx_webhooks.utils.clear_memoized_values() -@pytest.fixture(params=[ - pytest.param(False, id="pr:closed"), - pytest.param(True, id="pr:merged"), -]) +@pytest.fixture( + params=[ + pytest.param(False, id="pr:closed"), + pytest.param(True, id="pr:merged"), + ] +) def is_merged(request): """Makes tests try both merged and closed pull requests.""" return request.param diff --git a/tests/fake_github.py b/tests/fake_github.py index 07e407ff..06bf1f30 100644 --- a/tests/fake_github.py +++ b/tests/fake_github.py @@ -9,8 +9,9 @@ import datetime import itertools import random +from collections.abc import Iterable from dataclasses import dataclass, field -from typing import Any, Dict, Iterable, List, Optional, Set +from typing import Any from openedx_webhooks.cla_check import CLA_CONTEXT from openedx_webhooks.types import GhProject @@ -20,22 +21,27 @@ class FakeGitHubException(faker.FakerException): - def as_json(self) -> Dict: + def as_json(self) -> dict: j = {"message": str(self)} return j + class DoesNotExist(FakeGitHubException): """A requested object does not exist.""" + status_code = 404 + def fake_sha(): """A realistic stand-in for a commit sha.""" return "".join(random.choice("0123456789abcdef") for c in range(32)) + def fake_node_id(): """A plausible stand-in for a node id.""" return "NODE_" + "".join(random.choice("0123456789abcdef") for c in range(16)) + @dataclass class User: login: str = "some-user" @@ -55,12 +61,13 @@ def as_json(self): @dataclass class Label: name: str - color: Optional[str] = "ededed" - description: Optional[str] = None + color: str | None = "ededed" + description: str | None = None def as_json(self): return dataclasses.asdict(self) + DEFAULT_LABELS = [ {"name": "bug", "color": "d73a4a", "description": "Something isn't working"}, {"name": "documentation", "color": "0075ca", "description": "Improvements or additions to documentation"}, @@ -75,11 +82,13 @@ def as_json(self): comment_ids = itertools.count(start=1001, step=137) + @dataclass class Comment: """ A comment on an issue or pull request. """ + id: int = field(init=False, default_factory=comment_ids.__next__) user: User body: str @@ -90,7 +99,7 @@ def __post_init__(self): def validate(self): check_good_markdown(self.body) - def as_json(self) -> Dict: + def as_json(self) -> dict: return { "id": self.id, "body": self.body, @@ -109,20 +118,20 @@ class PullRequest: number: int user: User title: str = "" - body: Optional[str] = "" + body: str | None = "" node_id: str = field(default_factory=fake_node_id) created_at: datetime.datetime = field(default_factory=patchable_now) - closed_at: Optional[datetime.datetime] = None - merged_at: Optional[datetime.datetime] = None - comments: List[int] = field(default_factory=list) - labels: Set[str] = field(default_factory=set) + closed_at: datetime.datetime | None = None + merged_at: datetime.datetime | None = None + comments: list[int] = field(default_factory=list) + labels: set[str] = field(default_factory=set) state: str = "open" merged: bool = False draft: bool = False - commits: List[str] = field(default_factory=list) + commits: list[str] = field(default_factory=list) ref: str = "" - def as_json(self, brief=False) -> Dict: + def as_json(self, brief=False) -> dict: j = { "number": self.number, "node_id": self.node_id, @@ -172,7 +181,7 @@ def add_comment(self, user="someone", **kwargs) -> Comment: self.comments.append(comment.id) return comment - def list_comments(self) -> List[Comment]: + def list_comments(self) -> list[Comment]: return [com for cid in self.comments if (com := self.repo.comments.get(cid))] def delete_comment(self, comment_number) -> None: @@ -205,15 +214,15 @@ class Repo: owner: str repo: str private: bool - labels: Dict[str, Label] = field(default_factory=dict) - pull_requests: Dict[int, PullRequest] = field(default_factory=dict) - comments: Dict[int, Comment] = field(default_factory=dict) + labels: dict[str, Label] = field(default_factory=dict) + pull_requests: dict[int, PullRequest] = field(default_factory=dict) + comments: dict[int, Comment] = field(default_factory=dict) @property def full_name(self): return f"{self.owner}/{self.repo}" - def as_json(self) -> Dict: + def as_json(self) -> dict: return { "full_name": self.full_name, "name": self.repo, @@ -234,14 +243,14 @@ def make_pull_request(self, user="someone", number=None, **kwargs) -> PullReques self.github.pr_nodes[pr.node_id] = pr return pr - def list_pull_requests(self, state: str) -> List[PullRequest]: + def list_pull_requests(self, state: str) -> list[PullRequest]: return [pr for pr in self.pull_requests.values() if (state == "all") or (pr.state == state)] def get_pull_request(self, number: int) -> PullRequest: try: return self.pull_requests[number] except KeyError: - raise DoesNotExist(f"Pull request {self.full_name} #{number} does not exist") + raise DoesNotExist(f"Pull request {self.full_name} #{number} does not exist") from None def make_comment(self, user, **kwargs) -> Comment: user = self.github.get_user(user, create=True) @@ -253,12 +262,12 @@ def get_label(self, name: str) -> Label: try: return self.labels[name] except KeyError: - raise DoesNotExist(f"Label {self.full_name} {name!r} does not exist") + raise DoesNotExist(f"Label {self.full_name} {name!r} does not exist") from None def has_label(self, name: str) -> bool: return name in self.labels - def _set_labels(self, data: List[Dict]) -> None: + def _set_labels(self, data: list[dict]) -> None: self.labels = {} for kwargs in data: self.add_label(**kwargs) @@ -273,6 +282,7 @@ class Flaky404: """ A middleware to emulate flaky behavior of GitHub's. """ + def __init__(self, fraction_404): self.fraction_404 = fraction_404 self.paths = set() @@ -293,27 +303,26 @@ def middleware(self, request, context): class FakeGitHub(faker.Faker): - def __init__(self, login, fraction_404=0) -> None: super().__init__(host="https://api.github.com") if fraction_404: self.add_middleware(Flaky404(fraction_404).middleware) self.login = login - self.users: Dict[str, User] = {} - self.repos: Dict[str, Repo] = {} + self.users: dict[str, User] = {} + self.repos: dict[str, Repo] = {} # Map from PR node id to pull request. - self.pr_nodes: Dict[str, PullRequest] = {} + self.pr_nodes: dict[str, PullRequest] = {} # Map from Project node id to (orgname, number) pairs. - self.project_nodes: Dict[str, GhProject] = {} + self.project_nodes: dict[str, GhProject] = {} # Map from (orgname, number) project ids to project node id. - self.projects: Dict[GhProject, str] = {} + self.projects: dict[GhProject, str] = {} # Map from PR node id to Project node ids, and from Project node id # to PR node ids. - self.project_items: Dict[str, Set[str]] = collections.defaultdict(set) + self.project_items: dict[str, set[str]] = collections.defaultdict(set) - self.cla_statuses: Dict[str, Dict[str, str]] = {} + self.cla_statuses: dict[str, dict[str, str]] = {} def make_user(self, login: str, **kwargs) -> User: u = self.users[login] = User(login, **kwargs) @@ -328,7 +337,7 @@ def get_user(self, login: str, create: bool = False) -> User: raise DoesNotExist(f"User {login!r} does not exist") return user - def make_repo(self, owner: str, repo: str, private: bool=False) -> Repo: + def make_repo(self, owner: str, repo: str, private: bool = False) -> Repo: r = Repo(self, owner, repo, private) r._set_labels(DEFAULT_LABELS) self.repos[f"{owner}/{repo}"] = r @@ -338,7 +347,7 @@ def get_repo(self, owner: str, repo: str) -> Repo: try: return self.repos[f"{owner}/{repo}"] except KeyError: - raise DoesNotExist(f"Repo {owner}/{repo} does not exist") + raise DoesNotExist(f"Repo {owner}/{repo} does not exist") from None def make_pull_request(self, owner: str = "an-org", repo: str = "a-repo", **kwargs) -> PullRequest: """Convenience: make a repo and a pull request.""" @@ -349,18 +358,18 @@ def make_pull_request(self, owner: str = "an-org", repo: str = "a-repo", **kwarg # Users @faker.route(r"/user") - def _get_user(self, _match, _request, _context) -> Dict: + def _get_user(self, _match, _request, _context) -> dict: return {"login": self.login} @faker.route(r"/users/(?P[^/]+)") - def _get_users(self, match, _request, _context) -> Dict: + def _get_users(self, match, _request, _context) -> dict: # https://developer.github.com/v3/users/#get-a-user return self.users[match["login"]].as_json() # Organization repos @faker.route(r"/orgs/(?P[^/]+)/repos") - def _get_org_repos(self, match, _request, _context) -> List[Dict]: + def _get_org_repos(self, match, _request, _context) -> list[dict]: org_prefix = match["org"] + "/" repos = [repo for name, repo in self.repos.items() if name.startswith(org_prefix)] return [repo.as_json() for repo in repos] @@ -368,21 +377,21 @@ def _get_org_repos(self, match, _request, _context) -> List[Dict]: # Pull requests @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/pulls") - def _get_pulls(self, match, request, _context) -> List[Dict]: + def _get_pulls(self, match, request, _context) -> list[dict]: # https://developer.github.com/v3/pulls/#list-pull-requests r = self.get_repo(match["owner"], match["repo"]) state = request.qs.get("state", ["open"])[0] return [pr.as_json(brief=True) for pr in r.list_pull_requests(state)] @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/pulls/(?P\d+)") - def _get_pull(self, match, _request, _context) -> Dict: + def _get_pull(self, match, _request, _context) -> dict: # https://developer.github.com/v3/pulls/#get-a-pull-request r = self.get_repo(match["owner"], match["repo"]) pr = r.get_pull_request(int(match["number"])) return pr.as_json() @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/issues/(?P\d+)", "PATCH") - def _patch_issues(self, match, request, _context) -> Dict: + def _patch_issues(self, match, request, _context) -> dict: # https://developer.github.com/v3/issues/#update-an-issue r = self.get_repo(match["owner"], match["repo"]) pr = r.get_pull_request(int(match["number"])) @@ -395,31 +404,31 @@ def _patch_issues(self, match, request, _context) -> Dict: # Commmits @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/statuses/(?P[a-fA-F0-9]+)(\?.*)?") - def _get_pr_status_check(self, match, _request, _context) -> List[Dict[str, Any]]: + def _get_pr_status_check(self, match, _request, _context) -> list[dict[str, Any]]: sha: str = match["sha"] if sha in self.cla_statuses: return [self.cla_statuses[sha]] else: return [] - @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/statuses/(?P[a-fA-F0-9]+)(\?.*)?", 'POST') - def _post_pr_status_update(self, match, request, _context) -> List[Dict[str, Any]]: + @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/statuses/(?P[a-fA-F0-9]+)(\?.*)?", "POST") + def _post_pr_status_update(self, match, request, _context) -> list[dict[str, Any]]: data = request.json() - assert data['context'] == CLA_CONTEXT - self.cla_statuses[match['sha']] = data + assert data["context"] == CLA_CONTEXT + self.cla_statuses[match["sha"]] = data return [data] # Comments @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/issues/(?P\d+)/comments(\?.*)?") - def _get_issues_comments(self, match, _request, _context) -> List[Dict]: + def _get_issues_comments(self, match, _request, _context) -> list[dict]: # https://developer.github.com/v3/issues/comments/#list-issue-comments r = self.get_repo(match["owner"], match["repo"]) pr = r.get_pull_request(int(match["number"])) return [com.as_json() for cid in pr.comments if (com := r.comments.get(cid))] @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/issues/(?P\d+)/comments", "POST") - def _post_issues_comments(self, match, request, _context) -> Dict: + def _post_issues_comments(self, match, request, _context) -> dict: # https://developer.github.com/v3/issues/comments/#create-an-issue-comment r = self.get_repo(match["owner"], match["repo"]) pr = r.get_pull_request(int(match["number"])) @@ -427,7 +436,7 @@ def _post_issues_comments(self, match, request, _context) -> Dict: return comment.as_json() @faker.route(r"/repos/(?P[^/]+)/(?P[^/]+)/issues/comments/(?P\d+)", "PATCH") - def _patch_issues_comments(self, match, request, _context) -> Dict: + def _patch_issues_comments(self, match, request, _context) -> dict: # https://developer.github.com/v3/issues/comments/#update-an-issue-comment r = self.get_repo(match["owner"], match["repo"]) comment = r.comments[int(match["comment_id"])] @@ -445,7 +454,7 @@ def _delete_issues_comments(self, match, _request, context) -> None: # GraphQL @faker.route(r"/graphql", "POST") - def _graphql(self, _match, request, _context) -> Dict: + def _graphql(self, _match, request, _context) -> dict: """Dispatch a GraphQL request.""" data = request.json() query = data["query"] @@ -457,16 +466,14 @@ def _graphql(self, _match, request, _context) -> Dict: raise Exception(f"Unknown GraphQL slug in FakeGitHub: {slug = }") return method(**kwargs) - def _graphql_ProjectsForPr(self, owner: str, name: str, number: int) -> Dict: + def _graphql_ProjectsForPr(self, owner: str, name: str, number: int) -> dict: r = self.get_repo(owner, name) pr = r.get_pull_request(number) project_node_ids = self.project_items[pr.node_id] nodes = [] for node_id in project_node_ids: org, num = self.project_nodes[node_id] - nodes.append( - {"project": {"owner": {"login": org}, "number": num}, "id": node_id} - ) + nodes.append({"project": {"owner": {"login": org}, "number": num}, "id": node_id}) return { "data": { "repository": { @@ -479,35 +486,21 @@ def _graphql_ProjectsForPr(self, owner: str, name: str, number: int) -> Dict: } } - def _graphql_OrgProjectId(self, owner: str, number: int) -> Dict: + def _graphql_OrgProjectId(self, owner: str, number: int) -> dict: proj_id = f"PROJECT:{owner}.{number}" self.project_nodes[proj_id] = (owner, number) self.projects[(owner, number)] = proj_id - return { - "data": { - "organization": { - "projectV2": { - "id": proj_id - } - } - } - } + return {"data": {"organization": {"projectV2": {"id": proj_id}}}} - def _graphql_AddProjectItem(self, projectId: str, prNodeId: str) -> Dict: + def _graphql_AddProjectItem(self, projectId: str, prNodeId: str) -> dict: self.project_items[projectId].add(prNodeId) self.project_items[prNodeId].add(projectId) - return { - 'data': { - 'addProjectV2ItemById': { - 'item': {'id': 'saul goodman'} - } - } - } + return {"data": {"addProjectV2ItemById": {"item": {"id": "saul goodman"}}}} def _graphql_UpdateProjectItem(self, projectId: str, itemId: str, fieldId: str, value) -> dict: self.project_items[projectId].add(itemId) self.project_items[fieldId].add(value) - return {'data': {}} + return {"data": {}} def _graphql_OrgProjectMetadata(self, orgname: str, number: int) -> dict: proj_id = f"PROJECT:{orgname}.{number}" @@ -525,7 +518,7 @@ def _graphql_OrgProjectMetadata(self, orgname: str, number: int) -> dict: {"name": "Date merged/closed", "id": "date-closed-id", "dataType": "date"}, {"name": "Repo Owner / Owning Team", "id": "repo-owner-id", "dataType": "text"}, ] - } + }, } } } diff --git a/tests/fake_jira.py b/tests/fake_jira.py index c0e99170..b878fb15 100644 --- a/tests/fake_jira.py +++ b/tests/fake_jira.py @@ -4,13 +4,12 @@ import itertools import re from dataclasses import dataclass, field -from typing import Dict, Optional, Set from . import faker - issue_ids = itertools.count(start=101, step=13) + def _make_issue_key(project: str) -> str: """Generate the next issue key for a project.""" num = next(issue_ids) @@ -20,12 +19,13 @@ def _make_issue_key(project: str) -> str: @dataclass class Issue: """A Jira issue.""" + key: str status: str - issuetype: Optional[str] = None - description: Optional[str] = None - summary: Optional[str] = None - labels: Set[str] = field(default_factory=set) + issuetype: str | None = None + description: str | None = None + summary: str | None = None + labels: set[str] = field(default_factory=set) def __post_init__(self) -> None: # Jira labels can't have spaces in them. Check that they are only @@ -36,7 +36,7 @@ def __post_init__(self) -> None: if len(label) < 3: raise ValueError(f"Label {label!r} is too short") - def as_json(self) -> Dict: + def as_json(self) -> dict: return { "key": self.key, "fields": { @@ -60,20 +60,22 @@ class FakeJira(faker.Faker): # state. Other projects could be much more complex. TRANSITIONS = { name: str(i + 901) - for i, name in enumerate([ - "Needs Triage", - "Waiting on Author", - "Blocked by Other Work", - "Rejected", - "Merged", - "Community Manager Review", - "Open edX Community Review", - "Awaiting Prioritization", - "Product Review", - "Engineering Review", - "Architecture Review", - "Changes Requested", - ]) + for i, name in enumerate( + [ + "Needs Triage", + "Waiting on Author", + "Blocked by Other Work", + "Rejected", + "Merged", + "Community Manager Review", + "Open edX Community Review", + "Awaiting Prioritization", + "Product Review", + "Engineering Review", + "Architecture Review", + "Changes Requested", + ] + ) } TRANSITION_IDS = {id: name for name, id in TRANSITIONS.items()} @@ -81,11 +83,11 @@ class FakeJira(faker.Faker): def __init__(self, host) -> None: super().__init__(host=host) # Map from issue keys to Issue objects. - self.issues: Dict[str, Issue] = {} + self.issues: dict[str, Issue] = {} # Map from old keys to new keys for moved issues. - self.moves: Dict[str, str] = {} + self.moves: dict[str, str] = {} - def make_issue(self, key: Optional[str] = None, project: str = "OSPR", **kwargs) -> Issue: + def make_issue(self, key: str | None = None, project: str = "OSPR", **kwargs) -> Issue: """Make fake issue data.""" if key is None: key = _make_issue_key(project) @@ -93,7 +95,7 @@ def make_issue(self, key: Optional[str] = None, project: str = "OSPR", **kwargs) self.issues[key] = issue return issue - def find_issue(self, key: str) -> Optional[Issue]: + def find_issue(self, key: str) -> Issue | None: """ Find an issue, even across moves. @@ -115,7 +117,7 @@ def move_issue(self, issue: Issue, project: str) -> Issue: return the_issue @faker.route(r"/rest/api/2/issue/(?P\w+-\d+)") - def _get_issue(self, match, _request, context) -> Dict: + def _get_issue(self, match, _request, context) -> dict: """Implement the GET issue endpoint.""" if (issue := self.find_issue(match["key"])) is not None: return issue.as_json() @@ -130,12 +132,12 @@ def _post_issue(self, _match, request, context): fields = issue_data["fields"] project = fields["project"]["key"] key = _make_issue_key(project) - kwargs = dict( # pylint: disable=use-dict-literal - issuetype=fields["issuetype"]["name"], - summary=fields.get("summary"), - description=fields.get("description"), - labels=set(fields.get("labels")), - ) + kwargs = { + "issuetype": fields["issuetype"]["name"], + "summary": fields.get("summary"), + "description": fields.get("description"), + "labels": set(fields.get("labels")), + } self.make_issue(key, **kwargs) # Response is only some information: # {"id":"184975","key":"OSPR-4836","self":"https://test.atlassian.net/rest/api/2/issue/184975"} diff --git a/tests/faker.py b/tests/faker.py index 04dbb94c..a8a1f003 100644 --- a/tests/faker.py +++ b/tests/faker.py @@ -45,8 +45,10 @@ def _route_handler(self, match, request, context): data_type: "json" or "text", the type of data the function will return. """ + def _decorator(func): func.callback_spec = (path_regex, http_method.upper(), data_type) + @functools.wraps(func) def _decorated(self, request, context) -> Any: for fn in self.middleware: @@ -59,7 +61,9 @@ def _decorated(self, request, context) -> Any: except FakerException as ex: context.status_code = ex.status_code return ex.as_json() + return _decorated + return _decorator @@ -99,7 +103,7 @@ def install_mocks(self, requests_mocker) -> None: path_regex, http_method, data_type = method.callback_spec self.requests_mocker.register_uri( http_method, - re.compile(fr"^{self.host}{path_regex}(\?.*)?$"), + re.compile(rf"^{self.host}{path_regex}(\?.*)?$"), **{data_type: method}, ) diff --git a/tests/helpers.py b/tests/helpers.py index a153c526..2af574de 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -36,7 +36,7 @@ def check_good_markdown(text: str) -> None: raise ValueError(f"Markdown has a link to a None url: {text!r}") -def check_issue_link_in_markdown(text: str, jira_id: JiraId|None) -> None: +def check_issue_link_in_markdown(text: str, jira_id: JiraId | None) -> None: """ Check that `text` has properly formatted links to `jira_id`. @@ -89,7 +89,7 @@ def check_good_graphql(text: str) -> None: for ch in code: if ch in pairs.values(): stack.append(ch) - elif ch in pairs.keys(): # pylint: disable=consider-iterating-dictionary + elif ch in pairs.keys(): # pylint: disable=consider-iterating-dictionary if not stack or stack[-1] != pairs[ch]: raise ValueError(f"GraphQL query has unbalanced parens: {text!r}") stack.pop() diff --git a/tests/test_fake_github.py b/tests/test_fake_github.py index cc40545e..b4233769 100644 --- a/tests/test_fake_github.py +++ b/tests/test_fake_github.py @@ -2,15 +2,14 @@ import pytest import requests - from freezegun import freeze_time from glom import glom from .fake_github import FakeGitHub - # pylint: disable=missing-timeout + class TestUsers: def test_get_me(self, fake_github): resp = requests.get("https://api.github.com/user") @@ -117,11 +116,14 @@ def test_list_pull_requests(self, pull_requests_to_list): # When listing pull requests, not all fields are returned. assert not any(k in prj for prj in prjs for k in ["merged"]) - @pytest.mark.parametrize("state, number, specific", [ - ("open", 3, True), - ("closed", 2, True), - ("all", 5, False), - ]) + @pytest.mark.parametrize( + "state, number, specific", + [ + ("open", 3, True), + ("closed", 2, True), + ("all", 5, False), + ], + ) def test_list_pull_requests_count(self, pull_requests_to_list, state, number, specific): resp = requests.get(f"https://api.github.com/repos/an-org/a-repo/pulls?state={state}") prjs = resp.json() @@ -149,9 +151,7 @@ def test_updating_labels_with_api(self, fake_github): assert repo.get_label("bug").color == "d73a4a" assert repo.get_label("another label").color == "ededed" - resp = requests.get( - f"https://api.github.com/repos/an-org/a-repo/pulls/{pr.number}" - ) + resp = requests.get(f"https://api.github.com/repos/an-org/a-repo/pulls/{pr.number}") assert resp.status_code == 200 prj = resp.json() assert prj["title"] == "Here is a pull request" @@ -177,9 +177,7 @@ def test_updating_labels_elsewhere(self, fake_github): assert repo.get_label("bug").color == "d73a4a" assert repo.get_label("another label").color == "ededed" - resp = requests.get( - f"https://api.github.com/repos/an-org/a-repo/pulls/{pr.number}" - ) + resp = requests.get(f"https://api.github.com/repos/an-org/a-repo/pulls/{pr.number}") assert resp.status_code == 200 prj = resp.json() assert prj["title"] == "Here is a pull request" @@ -196,17 +194,13 @@ def test_listing_comments(self, fake_github): repo = fake_github.make_repo("an-org", "a-repo") pr = repo.make_pull_request() assert pr.comments == [] - resp = requests.get( - f"https://api.github.com/repos/an-org/a-repo/issues/{pr.number}/comments" - ) + resp = requests.get(f"https://api.github.com/repos/an-org/a-repo/issues/{pr.number}/comments") assert resp.status_code == 200 assert resp.json() == [] pr.add_comment(user="tusbar", body="This is my comment") pr.add_comment(user="feanil", body="I love this change!") - resp = requests.get( - f"https://api.github.com/repos/an-org/a-repo/issues/{pr.number}/comments" - ) + resp = requests.get(f"https://api.github.com/repos/an-org/a-repo/issues/{pr.number}/comments") assert resp.status_code == 200 summary = glom(resp.json(), [{"u": "user.login", "b": "body"}]) assert summary == [ @@ -308,6 +302,7 @@ def flaky_github(requests_mocker, fake_repo_data): the_fake_github.install_mocks(requests_mocker) return the_fake_github + class TestFlakyGitHub: def test_get(self, flaky_github): # The first time we request something, it's 404, and then it's OK after that. diff --git a/tests/test_fake_jira.py b/tests/test_fake_jira.py index e79f0344..8aa4f5b9 100644 --- a/tests/test_fake_jira.py +++ b/tests/test_fake_jira.py @@ -3,13 +3,14 @@ import pytest import requests - # pylint: disable=missing-timeout + class TestIssues: """ Tests of the correct behavior of issuees. """ + def test_get_issue(self, fake_jira): fake_jira.make_issue(key="HELLO-123", summary="This is a bad bug!", labels=["bad-bug"]) resp = requests.get("https://test.atlassian.net/rest/api/2/issue/HELLO-123") @@ -48,7 +49,7 @@ def test_move_issue(self, fake_jira): resp = requests.get(f"https://test.atlassian.net/rest/api/2/issue/{key1}") assert resp.status_code == 200 jissue1 = resp.json() - assert jissue1["key"] == key2 # it has the new key. + assert jissue1["key"] == key2 # it has the new key. assert jissue1["fields"]["summary"] == "This is a bad bug!" # Look it up under the new key. @@ -72,6 +73,7 @@ class TestBadRequests: """ Tests of the error edge cases. """ + def test_no_such_put(self, fake_jira): resp = requests.put("https://test.atlassian.net/rest/api/2/issue/XYZ-999") assert resp.status_code == 404 diff --git a/tests/test_faker.py b/tests/test_faker.py index 4b22b01c..8ab042e2 100644 --- a/tests/test_faker.py +++ b/tests/test_faker.py @@ -8,9 +8,9 @@ from . import faker - # pylint: disable=missing-timeout + class MyException(faker.FakerException): status_code = 501 @@ -19,6 +19,7 @@ class MyFake(faker.Faker): """ Our Faker-derived fake API for these tests. """ + def __init__(self, host): super().__init__(host) self.add_middleware(self.no_foo_middleware) @@ -76,38 +77,46 @@ def test_json_data(my_fake): assert resp.status_code == 200 assert resp.json() == {"hello": "there", "id": "ME-123"} + def test_post(my_fake): resp = requests.post("https://myapi.com/api/something/ME-456") assert resp.status_code == 200 assert resp.json() == {"created": "ME-456"} + def test_exception(my_fake): resp = requests.get("https://myapi.com/api/bad") assert resp.status_code == 501 assert resp.json() == {"error": "Bad!"} + def test_query_and_status(my_fake): resp = requests.get("https://myapi.com/api/status?code=477") assert resp.status_code == 477 assert resp.text == "" + def test_middleware(my_fake): """Middleware can interrupt handler execution.""" resp = requests.get("https://myapi.com/api/status?code=477&foo") assert resp.status_code == 789 -@pytest.mark.parametrize("method, url", [ - ("GET", "https://myapi.com/nothing"), - ("GET", "https://myapi.com"), - ("POST", "https://myapi.com/api/status"), - ("GET", "http://myapi.com/api/something/ME-123"), - ("GET", "https://otherapi.com/api/something/ME-123"), -]) +@pytest.mark.parametrize( + "method, url", + [ + ("GET", "https://myapi.com/nothing"), + ("GET", "https://myapi.com"), + ("POST", "https://myapi.com/api/status"), + ("GET", "http://myapi.com/api/something/ME-123"), + ("GET", "https://otherapi.com/api/something/ME-123"), + ], +) def test_no_address(my_fake, method, url): with pytest.raises(requests_mock.NoMockAddress): requests.request(method, url) + def test_requests_made(my_fake): requests.get("https://myapi.com/api/something/1") requests.get("https://myapi.com/api/something/1234") @@ -132,6 +141,7 @@ def test_requests_made(my_fake): ("/api/something/1234", "GET"), ] + def test_reset_mock(my_fake): requests.get("https://myapi.com/api/something/1") requests.get("https://myapi.com/api/something/1234") @@ -144,12 +154,14 @@ def test_reset_mock(my_fake): ("/api/something/bug123", "DELETE"), ] + def test_readonly(my_fake): requests.get("https://myapi.com/api/something/1") requests.get("https://myapi.com/api/something/1234") requests.get("https://some.other.host/") my_fake.assert_readonly() + def test_not_readonly(my_fake): requests.get("https://myapi.com/api/something/1") requests.get("https://myapi.com/api/something/1234") diff --git a/tests/test_gh_projects.py b/tests/test_gh_projects.py index cb97b539..5941a8ba 100644 --- a/tests/test_gh_projects.py +++ b/tests/test_gh_projects.py @@ -18,7 +18,7 @@ def test_adding_pr_to_project(fake_github): add_pull_request_to_project(prid, pr.node_id, ("myorg", 23)) project_info = pull_request_projects_info(prj) - assert project_info == [{'id': 'PROJECT:myorg.23', 'org': 'myorg', 'number': 23}] + assert project_info == [{"id": "PROJECT:myorg.23", "org": "myorg", "number": 23}] projects = set(pull_request_projects(prj, project_info)) assert projects == {("myorg", 23)} assert pr.is_in_project(("myorg", 23)) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index b1ef0a4b..7b7f1ebe 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -5,17 +5,20 @@ from .helpers import check_good_graphql, check_good_markdown, random_text -@pytest.mark.parametrize("text, ok, msg", [ - ("This is a paragraph", True, ""), - ("This is a paragraph\n\nThis is also\n", True, ""), - (" Bad: initial space", False, "start with whitespace"), - ("\nA paragraph", True, ""), - ("A paragraph", False, "comment with following text"), - ("Trailing comment\n", False, "comment in the middle"), - ("Look here: [None](https://foo.com).", False, "link to None"), - ("Look here: [foo](https://foo.com/api/id/None).", False, "link to a None"), - ("Look here: [foo](https://foo.com/api/id/None/comments).", False, "link to a None"), -]) +@pytest.mark.parametrize( + "text, ok, msg", + [ + ("This is a paragraph", True, ""), + ("This is a paragraph\n\nThis is also\n", True, ""), + (" Bad: initial space", False, "start with whitespace"), + ("\nA paragraph", True, ""), + ("A paragraph", False, "comment with following text"), + ("Trailing comment\n", False, "comment in the middle"), + ("Look here: [None](https://foo.com).", False, "link to None"), + ("Look here: [foo](https://foo.com/api/id/None).", False, "link to a None"), + ("Look here: [foo](https://foo.com/api/id/None/comments).", False, "link to a None"), + ], +) def test_check_good_markdown(text, ok, msg): if ok: assert msg == "" @@ -26,16 +29,19 @@ def test_check_good_markdown(text, ok, msg): def test_random_text(): - texts = set(random_text() for _ in range(10)) + texts = {random_text() for _ in range(10)} assert len(texts) == 10 assert "" not in texts -@pytest.mark.parametrize("text, ok, msg", [ - ("query { org }", True, ""), - ("# This is GraphQL!\n query Hello {org}\n", True, ""), - (" what { org }", False, "wrong word"), - ("""\ +@pytest.mark.parametrize( + "text, ok, msg", + [ + ("query { org }", True, ""), + ("# This is GraphQL!\n query Hello {org}\n", True, ""), + (" what { org }", False, "wrong word"), + ( + """\ query OrgProjectId ( $owner: String! $number: Int! @@ -46,11 +52,15 @@ def test_random_text(): } } } - """, True, ""), - ("query (hi {ord}", False, "balanced"), - ("query hello )hi {ord}", False, "balanced"), - ("query (((}", False, "balanced"), - ("""\ + """, + True, + "", + ), + ("query (hi {ord}", False, "balanced"), + ("query hello )hi {ord}", False, "balanced"), + ("query (((}", False, "balanced"), + ( + """\ mutation AddProjectItem ( $projectId: String! $prNodeId: String! @@ -61,8 +71,12 @@ def test_random_text(): } } } - """, False, "balanced"), -]) + """, + False, + "balanced", + ), + ], +) def test_check_good_graphql(text, ok, msg): if ok: assert msg == "" diff --git a/tests/test_info.py b/tests/test_info.py index 9edf3365..43412a65 100644 --- a/tests/test_info.py +++ b/tests/test_info.py @@ -6,15 +6,14 @@ from openedx_webhooks.info import ( get_blended_project_id, + get_catalog_info, get_jira_info, get_people_file, is_draft_pull_request, is_internal_pull_request, jira_details_for_pr, - get_catalog_info, ) - # These tests should run when we want to test flaky GitHub behavior. pytestmark = [ pytest.mark.flaky_github, @@ -27,11 +26,13 @@ def make_pull_request(fake_github): """ Provide a function for making a JSON pull request object. """ + def _fn(user, repo="openedx/edx-platform", **kwargs): fake_github.make_user(user) owner, repo = repo.split("/") pr = fake_github.make_pull_request(user=user, owner=owner, repo=repo, **kwargs) return pr.as_json() + return _fn @@ -41,38 +42,48 @@ def test_edx_employee(make_pull_request): pr = make_pull_request("nedbat", repo="edx/something") assert is_internal_pull_request(pr) + def test_tcril_employee(make_pull_request): pr = make_pull_request("feanil") assert is_internal_pull_request(pr) pr = make_pull_request("feanil", repo="edx/something") assert not is_internal_pull_request(pr) + def test_ex_edx_employee(make_pull_request): pr = make_pull_request("mmprandom") assert not is_internal_pull_request(pr) + def test_never_heard_of_you(make_pull_request): pr = make_pull_request("some_random_guy") assert not is_internal_pull_request(pr) + def test_hourly_worker(make_pull_request): pr = make_pull_request("theJohnnyBrown") assert not is_internal_pull_request(pr) + def test_left_but_still_a_fan(make_pull_request): pr = make_pull_request("jarv") assert not is_internal_pull_request(pr) + def test_current_person_no_institution(): people = get_people_file() current_person = people["jarv"] assert "institution" not in current_person assert current_person["agreement"] == "individual" -@pytest.mark.parametrize("username, is_cc", [ - ("feanil", True), - ("jarv", False), -]) + +@pytest.mark.parametrize( + "username, is_cc", + [ + ("feanil", True), + ("jarv", False), + ], +) def test_person_is_core_contributor(username, is_cc): people = get_people_file() current_person = people[username] @@ -84,15 +95,19 @@ def test_current_person(): current_person = people["raisingarizona"] assert current_person["agreement"] == "none" -@pytest.mark.parametrize("title, number", [ - ("Please take my change", None), - ("[BD-17] Fix typo", 17), - ("This is for [ BD-007]", 7), - ("This is for [ BD - 0070 ]", 70), - ("Blended BD-18 doesn't count", None), - ("[BD-34] [BB-1234] extra tags are OK", 34), - ("[BB-1234] [BD-34] extra tags are OK", 34), -]) + +@pytest.mark.parametrize( + "title, number", + [ + ("Please take my change", None), + ("[BD-17] Fix typo", 17), + ("This is for [ BD-007]", 7), + ("This is for [ BD - 0070 ]", 70), + ("Blended BD-18 doesn't count", None), + ("[BD-34] [BB-1234] extra tags are OK", 34), + ("[BB-1234] [BD-34] extra tags are OK", 34), + ], +) def test_get_blended_project_id(fake_github, title, number): pr = fake_github.make_pull_request(title=title) num = get_blended_project_id(pr.as_json()) @@ -109,36 +124,41 @@ def test_get_blended_project_id(fake_github, title, number): ("This is wip, not ready yet", True), ] + @pytest.mark.parametrize("title, is_wip", TITLES_WIP) def test_is_wip_pull_request(fake_github, title, is_wip): # A PR is draft if it has a WIP title. pr = fake_github.make_pull_request(title=title) assert is_draft_pull_request(pr.as_json()) == is_wip + @pytest.mark.parametrize("title", [p[0] for p in TITLES_WIP]) def test_is_draft_pull_request(fake_github, title): # No matter what the title, a pr is Draft if it says it is. pr = fake_github.make_pull_request(title=title, draft=True) assert is_draft_pull_request(pr.as_json()) + def test_check_csv_users_only(): people = get_people_file() - user = 'Carlos-Muniz' + user = "Carlos-Muniz" # This user exists in the yaml but not in the csv, and should not be in people assert people.get(user) is None + def test_check_csv_org_priority(): people = get_people_file() - user = 'mmprandom' - assert people[user]['agreement'] == 'individual' + user = "mmprandom" + assert people[user]["agreement"] == "individual" + def test_check_people_missing_yaml_fields(): people = get_people_file() - user = 'test-test' - assert people[user].get('jira') is None - assert people[user].get('commiter') is None - assert people[user].get('comments') is None - assert people[user].get('before') is None + user = "test-test" + assert people[user].get("jira") is None + assert people[user].get("commiter") is None + assert people[user].get("comments") is None + assert people[user].get("before") is None def test_jira_info(): @@ -146,8 +166,7 @@ def test_jira_info(): # These are specific items from our test jira-info.yaml file assert info["test1"].server == "https://test.atlassian.net" assert ( - info["anotherorg"].mapping == - "https://raw.githubusercontent.com/anotherorg/dot-github/HEAD/jira-mapping.yaml" + info["anotherorg"].mapping == "https://raw.githubusercontent.com/anotherorg/dot-github/HEAD/jira-mapping.yaml" ) @@ -157,7 +176,7 @@ def test_jira_info(): ("openedx", "edx-platform", "ARCHBOM", "Bug"), ("nedbat", "anything", "NEDBAT", "Task"), ("another", "something", "EVERYTHING", "Task"), - ] + ], ) def test_jira_details_for_pr(fake_github, owner, repo, project, issuetype): pr = fake_github.make_pull_request(owner=owner, repo=repo) @@ -167,22 +186,13 @@ def test_jira_details_for_pr(fake_github, owner, repo, project, issuetype): def test_get_malformed_catalog_info(mocker, caplog): - mocker.patch( - "openedx_webhooks.info.read_github_file", - lambda *args,**kwargs: "bad: 'yaml" - ) + mocker.patch("openedx_webhooks.info.read_github_file", lambda *args, **kwargs: "bad: 'yaml") info = get_catalog_info("foo") assert info == {} assert "Failed to parse catalog-info.yaml file in foo." in caplog.text + def test_get_valid_catalog_info(mocker): - mocker.patch( - "openedx_webhooks.info.read_github_file", - lambda *args,**kwargs: "good: 'yaml'" - ) + mocker.patch("openedx_webhooks.info.read_github_file", lambda *args, **kwargs: "good: 'yaml'") info = get_catalog_info("foo") assert info == {"good": "yaml"} - - - - diff --git a/tests/test_pr_comments.py b/tests/test_pr_comments.py index a52c1aed..1751d860 100644 --- a/tests/test_pr_comments.py +++ b/tests/test_pr_comments.py @@ -6,11 +6,11 @@ from openedx_webhooks.bot_comments import ( BotComment, - is_comment_kind, - github_community_pr_comment, - github_end_survey_comment, extract_data_from_comment, format_data_for_comment, + github_community_pr_comment, + github_end_survey_comment, + is_comment_kind, ) from .helpers import check_good_markdown @@ -60,6 +60,7 @@ def test_survey_pr_comment(fake_github, is_merged): "lists": [1, 2, 3, [4, 5, 6]], } + def test_data_in_comments(): comment = "blah blah" + format_data_for_comment(COMMENT_DATA) check_good_markdown(comment) diff --git a/tests/test_pull_request_closed.py b/tests/test_pull_request_closed.py index 5c9d04aa..d42a3e68 100644 --- a/tests/test_pull_request_closed.py +++ b/tests/test_pull_request_closed.py @@ -36,7 +36,7 @@ def closed_pull_request(is_merged, fake_github, fake_jira): owner="openedx", user="tusbar", title=random_text(), - ) + ) result = pull_request_changed(pr.as_json()) assert not result.jira_issues pr.add_comment(user="nedbat", body="Please make some changes") @@ -56,7 +56,7 @@ def test_external_pr_closed_but_issue_deleted(fake_jira, closed_pull_request): assert not result.jira_issues pr_comments = pr.list_comments() - assert len(pr_comments) == 3 # 1 welcome, closed_pull_request makes two + assert len(pr_comments) == 3 # 1 welcome, closed_pull_request makes two # We leave the old issue id in the comment. body = pr_comments[0].body check_issue_link_in_markdown(body, None) @@ -84,13 +84,13 @@ def test_cc_pr_closed(fake_github, fake_jira, is_merged): pull_request_changed(pr.as_json()) pr_comments = pr.list_comments() - assert len(pr_comments) == 1 # welcome comment + assert len(pr_comments) == 1 # welcome comment # Processing it again won't change anything. pull_request_changed(pr.as_json()) pr_comments = pr.list_comments() - assert len(pr_comments) == 1 # welcome comment + assert len(pr_comments) == 1 # welcome comment @pytest.mark.parametrize("org", ["openedx", "edx"]) @@ -141,6 +141,6 @@ def test_pr_closed_labels(fake_github, is_merged): def test_pr_closed_date_on_close(closed_pull_request): pr = closed_pull_request pull_request_changed(pr.as_json()) - assert pr.repo.github.project_items['date-closed-id'] == { - pr.closed_at.isoformat(timespec='seconds') + 'Z', + assert pr.repo.github.project_items["date-closed-id"] == { + pr.closed_at.isoformat(timespec="seconds") + "Z", } diff --git a/tests/test_pull_request_opened.py b/tests/test_pull_request_opened.py index d213dd7e..97e26479 100644 --- a/tests/test_pull_request_opened.py +++ b/tests/test_pull_request_opened.py @@ -22,6 +22,7 @@ ) from openedx_webhooks.gh_projects import pull_request_projects from openedx_webhooks.tasks.github import pull_request_changed +from openedx_webhooks.tasks.pr_tracking import PrCurrentInfo, PrDesiredInfo, PrTrackingFixer from .helpers import check_issue_link_in_markdown @@ -78,17 +79,18 @@ def test_pr_in_nocontrib_repo_opened(fake_github, user): assert pull_request_projects(pr.as_json()) == set() -@pytest.mark.parametrize("owner,tag", [ - ("group:arch-bom", "@openedx/arch-bom"), - ("user:feanil", "@feanil"), - ("feanil", "@feanil"), -]) +@pytest.mark.parametrize( + "owner,tag", + [ + ("group:arch-bom", "@openedx/arch-bom"), + ("user:feanil", "@feanil"), + ("feanil", "@feanil"), + ], +) @mock.patch("openedx_webhooks.info.get_catalog_info") def test_pr_with_owner_repo_opened(get_catalog_info, fake_github, owner, tag, mocker): mocker.patch("openedx_webhooks.tasks.pr_tracking.get_github_user_info", lambda x: {"name": x}) - get_catalog_info.return_value = { - 'spec': {'owner': owner, 'lifecycle': 'production'} - } + get_catalog_info.return_value = {"spec": {"owner": owner, "lifecycle": "production"}} pr = fake_github.make_pull_request(owner="openedx", repo="edx-platform") result = pull_request_changed(pr.as_json()) assert not result.jira_issues @@ -101,9 +103,7 @@ def test_pr_with_owner_repo_opened(get_catalog_info, fake_github, owner, tag, mo @pytest.mark.parametrize("lifecycle", ["production", "deprecated", None]) @mock.patch("openedx_webhooks.info.get_catalog_info") def test_pr_without_owner_repo_opened(get_catalog_info, fake_github, lifecycle): - get_catalog_info.return_value = { - 'spec': {'lifecycle': lifecycle} - } if lifecycle else None + get_catalog_info.return_value = {"spec": {"lifecycle": lifecycle}} if lifecycle else None pr = fake_github.make_pull_request(owner="openedx", repo="edx-platform") result = pull_request_changed(pr.as_json()) assert not result.jira_issues @@ -338,14 +338,20 @@ def test_add_to_multiple_projects(fake_github): pr = fake_github.make_pull_request(owner="anotherorg", repo="multi-project", user="tusbar") pull_request_changed(pr.as_json()) assert pull_request_projects(pr.as_json()) == { - settings.GITHUB_OSPR_PROJECT, ("openedx", 23), ("anotherorg", 17), + settings.GITHUB_OSPR_PROJECT, + ("openedx", 23), + ("anotherorg", 17), } -@pytest.mark.parametrize("username, cc_label", [ - ("feanil", False), # CC but not OSPR - ("pdpinch", True), # CC - ("jarv", False), # Not CC -]) + +@pytest.mark.parametrize( + "username, cc_label", + [ + ("feanil", False), # CC but not OSPR + ("pdpinch", True), # CC + ("jarv", False), # Not CC + ], +) def test_automatic_core_contributor_label(fake_github, username, cc_label): pr = fake_github.make_pull_request(owner="openedx", repo="some-code", user=username, title="fix: XYZ") prj = pr.as_json() @@ -355,11 +361,15 @@ def test_automatic_core_contributor_label(fake_github, username, cc_label): else: assert "core contributor" not in pr.labels -@pytest.mark.parametrize("username", [ - "feanil", # CC but not OSPR - "pdpinch", # CC - "jarv", # Not CC -]) + +@pytest.mark.parametrize( + "username", + [ + "feanil", # CC but not OSPR + "pdpinch", # CC + "jarv", # Not CC + ], +) def test_manual_core_contributor_label(fake_github, username): pr = fake_github.make_pull_request(owner="openedx", repo="some-code", user=username, title="fix: XYZ") pr.set_labels(["core contributor"]) @@ -367,6 +377,7 @@ def test_manual_core_contributor_label(fake_github, username): pull_request_changed(prj) assert "core contributor" in pr.labels + def test_crash_label(fake_github): pr = fake_github.make_pull_request("openedx", user="nedbat") pr.set_labels(["crash!123"]) @@ -399,8 +410,7 @@ def test_jira_labelling(fake_github, fake_jira, fake_jira_another): Line1 Line2 - """ - ) + """) assert jira_issue.labels == {"from-GitHub"} # Processing the pull request again won't make another issue. @@ -484,10 +494,7 @@ def test_bad_jira_labelling_no_repo_map(fake_github, fake_jira2, mocker): assert len(pr.list_comments()) == 1 # The repo gets a mapping to a project. - mocker.patch( - "openedx_webhooks.tasks.pr_tracking.jira_details_for_pr", - lambda nick, pr: ("NEWPROJ", "Task") - ) + mocker.patch("openedx_webhooks.tasks.pr_tracking.jira_details_for_pr", lambda nick, pr: ("NEWPROJ", "Task")) # Processing the PR again won't add another comment. pull_request_changed(pr.as_json()) @@ -513,33 +520,45 @@ def test_bad_jira_labelling_no_repo_map(fake_github, fake_jira2, mocker): @pytest.mark.parametrize("owner", ["user:navin", "group:auth-group"]) def test_pr_project_fields_data(fake_github, mocker, owner): # Create user "navin" to fake `get_github_user_info` api. - fake_github.make_user(login='navin', name='Navin') + fake_github.make_user(login="navin", name="Navin") mocker.patch( - "openedx_webhooks.info.get_catalog_info", - lambda _: { - 'spec': {'owner': owner, 'lifecycle': 'production'} - } + "openedx_webhooks.info.get_catalog_info", lambda _: {"spec": {"owner": owner, "lifecycle": "production"}} ) created_at = datetime(2024, 12, 1) pr = fake_github.make_pull_request(owner="openedx", repo="edx-platform", created_at=created_at) pull_request_changed(pr.as_json()) - assert pr.repo.github.project_items['date-opened-id'] == {created_at.isoformat() + 'Z'} - assert pr.repo.github.project_items['date-closed-id'] == set() + assert pr.repo.github.project_items["date-opened-id"] == {created_at.isoformat() + "Z"} + assert pr.repo.github.project_items["date-closed-id"] == set() owner_type, owner_name = owner.split(":") if owner_type == "user": - assert pr.repo.github.project_items['repo-owner-id'] == {f"{owner_name.title()} (@{owner_name})"} + assert pr.repo.github.project_items["repo-owner-id"] == {f"{owner_name.title()} (@{owner_name})"} else: - assert pr.repo.github.project_items['repo-owner-id'] == {f"openedx/{owner_name}"} + assert pr.repo.github.project_items["repo-owner-id"] == {f"openedx/{owner_name}"} + + +def test_pr_project_fields_skipped_when_no_ospr_project(mocker): + """_fix_project_node_fields returns early without error when GITHUB_OSPR_PROJECT is None.""" + mocker.patch("openedx_webhooks.settings.GITHUB_OSPR_PROJECT", None) + + pr_dict = { + "base": {"repo": {"full_name": "openedx/edx-platform"}}, + "number": 42, + } + current = PrCurrentInfo() + current.github_projects_info = [{"id": "item-1", "org": "someorg", "number": 5}] + + mock_actions = mocker.MagicMock() + fixer = PrTrackingFixer(pr=pr_dict, current=current, desired=PrDesiredInfo(), actions=mock_actions) + fixer._fix_project_node_fields() + + mock_actions.update_project_pr_custom_field.assert_not_called() def test_pr_project_fields_invalid_field_name(fake_github, mocker, caplog): # Create user "navin" to fake `get_github_user_info` api. - fake_github.make_user(login='navin', name='Navin') + fake_github.make_user(login="navin", name="Navin") mocker.patch( - "openedx_webhooks.info.get_catalog_info", - lambda _: { - 'spec': {'owner': "user:navin", 'lifecycle': 'production'} - } + "openedx_webhooks.info.get_catalog_info", lambda _: {"spec": {"owner": "user:navin", "lifecycle": "production"}} ) # mock project metadata mocker.patch( @@ -548,18 +567,18 @@ def test_pr_project_fields_invalid_field_name(fake_github, mocker, caplog): "id": "some-project-id", "fields": [ {"name": "Name", "id": "name-id", "dataType": "text"}, - ] - } + ], + }, ) created_at = datetime(2024, 12, 1) pr = fake_github.make_pull_request(owner="openedx", repo="edx-platform", created_at=created_at) pull_request_changed(pr.as_json()) - assert pr.repo.github.project_items['date-opened-id'] == set() - assert pr.repo.github.project_items['repo-owner-id'] == set() + assert pr.repo.github.project_items["date-opened-id"] == set() + assert pr.repo.github.project_items["repo-owner-id"] == set() error_logs = [log for log in caplog.records if log.levelno == logging.ERROR] expected_msgs = ( f"Could not find field with name: Date opened in project: {settings.GITHUB_OSPR_PROJECT}", - f"Could not find field with name: Repo Owner / Owning Team in project: {settings.GITHUB_OSPR_PROJECT}" + f"Could not find field with name: Repo Owner / Owning Team in project: {settings.GITHUB_OSPR_PROJECT}", ) assert error_logs[0].msg == expected_msgs[0] assert error_logs[1].msg == expected_msgs[1] diff --git a/tests/test_rescan.py b/tests/test_rescan.py index 03b09e8b..f73ba5e1 100644 --- a/tests/test_rescan.py +++ b/tests/test_rescan.py @@ -38,17 +38,26 @@ def make_rescannable_repo(fake_github, org_name="an-org", repo_name="a-repo"): # Numbers of internal pull requsts are odd, external are even. repo.make_pull_request(user="nedbat", number=101, created_at=datetime(2019, 1, 1)) repo.make_pull_request(user="tusbar", number=102, created_at=datetime(2019, 2, 1)) - repo.make_pull_request(user="nedbat", number=103, state="closed", created_at=datetime(2019, 3, 1), - closed_at=datetime(2020,7,1)) + repo.make_pull_request( + user="nedbat", number=103, state="closed", created_at=datetime(2019, 3, 1), closed_at=datetime(2020, 7, 1) + ) repo.make_pull_request(user="feanil", number=105, created_at=datetime(2019, 4, 1)) repo.make_pull_request(user="tusbar", number=106, created_at=datetime(2019, 5, 1)) # A closed PR that had been processed when it opened. - pr = repo.make_pull_request(user="tusbar", number=108, created_at=datetime(2019, 6, 1), closed_at=datetime(2020,7,1)) + pr = repo.make_pull_request( + user="tusbar", number=108, created_at=datetime(2019, 6, 1), closed_at=datetime(2020, 7, 1) + ) pull_request_changed(pr.as_json()) pr.close(merge=False) # One of the PRs already has a bot comment with a Jira issue. - pr = repo.make_pull_request(user="tusbar", number=110, state="closed", merged=True, created_at=datetime(2019, 7, 1), - closed_at=datetime(2020,7,1)) + pr = repo.make_pull_request( + user="tusbar", + number=110, + state="closed", + merged=True, + created_at=datetime(2019, 7, 1), + closed_at=datetime(2020, 7, 1), + ) pr.add_comment(user=get_bot_username(), body=github_community_pr_comment(pr.as_json())) # Issues before 2018 should not be rescanned. @@ -58,10 +67,13 @@ def make_rescannable_repo(fake_github, org_name="an-org", repo_name="a-repo"): return repo -@pytest.mark.parametrize("allpr", [ - pytest.param(False, id="allpr:no"), - pytest.param(True, id="allpr:yes"), -]) +@pytest.mark.parametrize( + "allpr", + [ + pytest.param(False, id="allpr:no"), + pytest.param(True, id="allpr:yes"), + ], +) def test_rescan_repository(rescannable_repo, pull_request_changed_fn, allpr): ret = rescan_repository(rescannable_repo.full_name, allpr=allpr) changed = ret["changed"] @@ -102,22 +114,24 @@ def test_rescan_repository_dry_run(rescannable_repo, fake_github, fake_jira, pul # Get the names of the actions. We won't worry about the details, those # are tested in the non-dry-run tests of rescanning pull requests. - import json,sys; json.dump(ret["dry_run_actions"], sys.stdout, indent=4) + import sys + + json.dump(ret["dry_run_actions"], sys.stdout, indent=4) actions = {k: [name for name, kwargs in actions] for k, actions in ret["dry_run_actions"].items()} assert actions == { 102: [ "initial_state", - "set_cla_status", # "The author is authorized to contribute" - "update_labels_on_pull_request", # ["open-source-contribution"] - "add_comment_to_pull_request", # "Thanks for the pull request, @tusbar!" + "set_cla_status", # "The author is authorized to contribute" + "update_labels_on_pull_request", # ["open-source-contribution"] + "add_comment_to_pull_request", # "Thanks for the pull request, @tusbar!" "add_pull_request_to_project", "update_project_pr_custom_field", ], 106: [ "initial_state", - "set_cla_status", # "The author is authorized to contribute" - "update_labels_on_pull_request", # ["open-source-contribution"] - "add_comment_to_pull_request", # "Thanks for the pull request, @tusbar!" + "set_cla_status", # "The author is authorized to contribute" + "update_labels_on_pull_request", # ["open-source-contribution"] + "add_comment_to_pull_request", # "Thanks for the pull request, @tusbar!" "add_pull_request_to_project", "update_project_pr_custom_field", ], @@ -127,18 +141,22 @@ def test_rescan_repository_dry_run(rescannable_repo, fake_github, fake_jira, pul ], 110: [ "initial_state", - "set_cla_status", # "The author is authorized to contribute" + "set_cla_status", # "The author is authorized to contribute" ], } # The value returned should be json-encodable. json.dumps(ret) -@pytest.mark.parametrize("earliest, latest, nums", [ - ("", "", [102, 106, 108, 110]), - ("2019-06-01", "", [108, 110]), - ("2019-06-01", "2019-06-30", [108]), -]) + +@pytest.mark.parametrize( + "earliest, latest, nums", + [ + ("", "", [102, 106, 108, 110]), + ("2019-06-01", "", [108, 110]), + ("2019-06-01", "2019-06-30", [108]), + ], +) def test_rescan_repository_date_limits(rescannable_repo, pull_request_changed_fn, earliest, latest, nums): rescan_repository(rescannable_repo.full_name, allpr=True, earliest=earliest, latest=latest) @@ -158,12 +176,15 @@ def rescannable_org(fake_github): make_rescannable_repo(fake_github, org_name, repo_name) -@pytest.mark.parametrize("allpr, earliest, latest, nums", [ - (False, "", "", [102, 106]), - (True, "", "", [102, 106, 108, 110]), - (True, "2019-06-01", "", [108, 110]), - (True, "2019-06-01", "2019-06-30", [108]), -]) +@pytest.mark.parametrize( + "allpr, earliest, latest, nums", + [ + (False, "", "", [102, 106]), + (True, "", "", [102, 106, 108, 110]), + (True, "2019-06-01", "", [108, 110]), + (True, "2019-06-01", "2019-06-30", [108]), + ], +) def test_rescan_organization(rescannable_org, pull_request_changed_fn, allpr, earliest, latest, nums): rescan_organization("org1", allpr=allpr, earliest=earliest, latest=latest) prs = [PrId.from_pr_dict(c.args[0]) for c in pull_request_changed_fn.call_args_list] @@ -174,7 +195,7 @@ def test_rescan_organization(rescannable_org, pull_request_changed_fn, allpr, ea def test_rescan_failure(mocker, rescannable_repo): def flaky_pull_request_changed(pr, actions): if pr["number"] == 108: - return 1/0 # BOOM + return 1 / 0 # BOOM else: return pull_request_changed(pr, actions) @@ -185,5 +206,5 @@ def flaky_pull_request_changed(pr, actions): err = ret["errors"][108] assert err.startswith("Traceback (most recent call last):\n") assert " in flaky_pull_request_changed\n" in err - assert "1/0 # BOOM" in err + assert "1 / 0 # BOOM" in err assert "ZeroDivisionError: division by zero" in err diff --git a/tests/test_utils.py b/tests/test_utils.py index 3d15b573..f9bba39e 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,15 +18,27 @@ ) -@pytest.mark.parametrize("args, summary", [ - (["Hello"], "Hello"), - ([""], ""), - (["lorem ipsum quia dolor sit amet consecte"], "lorem ipsum quia dolor sit amet consecte"), - (["lorem ipsum quia dolor sit amet consectetur adipisci velit, sed quia non numquam eius modi tempora incidunt."], - "lorem ipsum quia d...i tempora incidunt."), - (["lorem ipsum quia dolor sit amet consectetur adipisci velit, quia non numquam eius modi tempora incidunt.", 80], - "lorem ipsum quia dolor sit amet consec...non numquam eius modi tempora incidunt."), -]) +@pytest.mark.parametrize( + "args, summary", + [ + (["Hello"], "Hello"), + ([""], ""), + (["lorem ipsum quia dolor sit amet consecte"], "lorem ipsum quia dolor sit amet consecte"), + ( + [ + "lorem ipsum quia dolor sit amet consectetur adipisci velit, sed quia non numquam eius modi tempora incidunt." + ], + "lorem ipsum quia d...i tempora incidunt.", + ), + ( + [ + "lorem ipsum quia dolor sit amet consectetur adipisci velit, quia non numquam eius modi tempora incidunt.", + 80, + ], + "lorem ipsum quia dolor sit amet consec...non numquam eius modi tempora incidunt.", + ), + ], +) def test_text_summary(args, summary): assert summary == text_summary(*args) @@ -50,20 +62,17 @@ def test_bad_graphql_query(requests_mocker): json={"errors": ["You blew it"]}, ) with pytest.raises(Exception, match=re.escape("GraphQL error: {'errors': ['You blew it']}")): - graphql_query("query Something {}", variables={"a":1, "b": 2}) + graphql_query("query Something {}", variables={"a": 1, "b": 2}) def _make_signature(secret, payload): """Compute a signature from a secret and a payload.""" - return ( - 'sha1=' + - hmac.new(secret.encode(), msg=payload, digestmod=hashlib.sha1).hexdigest() - ) + return "sha1=" + hmac.new(secret.encode(), msg=payload, digestmod=hashlib.sha1).hexdigest() SECRET1 = "top secret" SECRET2 = "not so top secret" -PAYLOAD = json.dumps('payload').encode("utf8") +PAYLOAD = json.dumps("payload").encode("utf8") def test_everything_matches(): @@ -83,12 +92,13 @@ def test_bad_secret(): def test_mismatched_payload(): signature = _make_signature(SECRET1, PAYLOAD) - wrong_payload = json.dumps('x').encode("utf8") + wrong_payload = json.dumps("x").encode("utf8") assert is_valid_payload(SECRET1, signature, wrong_payload) is False def test_memoize(): vals = [] + @memoize def add_to_vals(x): vals.append(x) @@ -107,8 +117,10 @@ def add_to_vals(x): assert add_to_vals(15) == 30 assert vals == [10, 15] + def test_memoize_timed(): vals = [] + @memoize_timed(minutes=10) def add_to_vals_timed(x): vals.append(x) @@ -132,8 +144,10 @@ def add_to_vals_timed(x): assert add_to_vals_timed(10) == 20 assert vals == [10, 15, 20, 10] + def test_clear_memoized_values(): vals = [] + @memoize def add_to_vals(x): vals.append(x) diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..affcb4b9 --- /dev/null +++ b/tox.ini @@ -0,0 +1,29 @@ +[tox] +envlist = py312-test, lint, mypy, docs +requires = tox-uv>=1 + +[testenv:lint] +runner = uv-venv-lock-runner +dependency_groups = quality +commands = + ruff check . + ruff format --check . + +[testenv:mypy] +runner = uv-venv-lock-runner +dependency_groups = quality +commands = mypy openedx_webhooks tests + +[testenv] +runner = uv-venv-lock-runner +dependency_groups = test +commands = pytest --cov=openedx_webhooks --cov=tests --cov-report=xml {posargs} + +[testenv:docs] +runner = uv-venv-lock-runner +basepython = python3.12 +changedir = {toxinidir}/docs +dependency_groups = doc +allowlist_externals = make +commands = + make html diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..d0651bc2 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2082 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[manifest] +constraints = [ + { name = "django", specifier = "<6.0" }, + { name = "elasticsearch", specifier = "<7.14.0" }, +] + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "amqp" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://files.pythonhosted.org/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://files.pythonhosted.org/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://files.pythonhosted.org/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://files.pythonhosted.org/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://files.pythonhosted.org/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://files.pythonhosted.org/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://files.pythonhosted.org/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://files.pythonhosted.org/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://files.pythonhosted.org/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "astroid" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "billiard" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, +] + +[[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, 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 = "boltons" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/1f/60df922ae497d838c58b48caa518251e2c8e228d7fe93792fee69a3858d6/boltons-26.0.0.tar.gz", hash = "sha256:5566d6cfd5a1e873d8e8476496287a9f92979964611ad9a9cecb6b0ef29b1edd", size = 225129, upload-time = "2026-06-19T06:10:41.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/01/4001033457f25ecdc2b1ffd513ca0b76200b9ea009dd64f6c1aad2dde133/boltons-26.0.0-py3-none-any.whl", hash = "sha256:ba077cac51b27532299634f87f5589b4080fa94a011b4d43a9247f775e9215c7", size = 195559, upload-time = "2026-06-19T06:10:39.865Z" }, +] + +[[package]] +name = "cachetools" +version = "7.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/8b/0d3945a13955303b81272f759a0331e54c5c793da455e6f5706b89d2639c/cachetools-7.1.4.tar.gz", hash = "sha256:437f55a4e0c1b01a4f3077cc470e6991d47430970e36fbcb77e2be0df4fc1cd6", size = 40085, upload-time = "2026-05-21T22:40:43.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/7b/1fc1c09cc0756cf25861a3be10565915953876da48bb228fb9a672b20a42/cachetools-7.1.4-py3-none-any.whl", hash = "sha256:323dc4127934744db5b54eb4924482d7edafbf9554e820d1531c2e08c0e4ef54", size = 16761, upload-time = "2026-05-21T22:40:41.845Z" }, +] + +[[package]] +name = "celery" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "billiard" }, + { name = "click" }, + { name = "click-didyoumean" }, + { name = "click-plugins" }, + { name = "click-repl" }, + { name = "kombu" }, + { name = "python-dateutil" }, + { name = "tzlocal" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz", hash = "sha256:177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", size = 1742243, upload-time = "2026-03-26T12:14:51.76Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl", hash = "sha256:0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", size = 451235, upload-time = "2026-03-26T12:14:49.491Z" }, +] + +[package.optional-dependencies] +redis = [ + { name = "kombu", extra = ["redis"] }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/56/10a88e00039537d74bd420f0457c52ab8f58a1af56126e3b9f1b1c8c4724/charset_normalizer-3.4.8.tar.gz", hash = "sha256:d9bf144d6faf12c70d58e47f7512992ae2882b820031d6cef68152deb645bf2d", size = 151790, upload-time = "2026-07-06T15:27:58.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/c2/39de60ef5687662f467bed3d1e6944c67a4f0d057141d0404002b8f405ae/charset_normalizer-3.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:faac37c4904598daa00cb4c9b32f3b4cc814fb5f145d7a531ceb4a70f2114132", size = 319040, upload-time = "2026-07-06T15:26:15.854Z" }, + { url = "https://files.pythonhosted.org/packages/a7/57/a9474c3aeaa337c8a330c0dc5df266527d56da3b189c029529f6b08af2a4/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f191c19a32dc6cec0fb8079789d786254653a9ce906fcab04ccd2eed07bba233", size = 215541, upload-time = "2026-07-06T15:26:17.265Z" }, + { url = "https://files.pythonhosted.org/packages/13/a9/be1ff7e81f6e086dced2a7a7a28b789be351d9796084ccaf6136a4ffafb3/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05811b76943d477bb90822dedb5c4565cef70148847a59d574e2b35043aeb563", size = 236913, upload-time = "2026-07-06T15:26:18.482Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/d8c5eae93da26d463f9ebe46a4937ca44434dc2937a565b92437befb3d94/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3868a3e4ec1e40b419e060d063f93eac6f046fa21426c4816421223ae7dc8ab8", size = 232815, upload-time = "2026-07-06T15:26:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/98e301ca944bcca5e6bc312406b579c8a6d81546c1b494afb3a9478495d6/charset_normalizer-3.4.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25f93d194eb6264c64416cabff46a91f6d99b97e7525a1b4f35c77a99e75cc68", size = 223995, upload-time = "2026-07-06T15:26:20.931Z" }, + { url = "https://files.pythonhosted.org/packages/fa/df/f5222366b76dcb31453a9bd922610c893540d0e729fd390439b0d3e972ee/charset_normalizer-3.4.8-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:ee6a62492f18d432cca031fabd158f400a8c25bf7b9458f50953393a2a23d97a", size = 208522, upload-time = "2026-07-06T15:26:22.214Z" }, + { url = "https://files.pythonhosted.org/packages/74/52/293220d59d8ddfb8aa56836b33bd6df58e70795d8a102a858c2984480f00/charset_normalizer-3.4.8-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c16cb4fc35e4b064f5ee78d849f15a550ada1729c3372916672e38f1f01d1d4", size = 219660, upload-time = "2026-07-06T15:26:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2c/81a298e66f3d01e61bfc6f7064bbb553b067a9f1d979e5962bf00733069b/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fbd0edb0426ab28e70fac9d1d4ef549eb5a64a2521f0428c441d75e4387e6c2", size = 218230, upload-time = "2026-07-06T15:26:24.629Z" }, + { url = "https://files.pythonhosted.org/packages/0d/45/f1dd2328cbc3340705f82072c09bd4c68d6e079191cde05810c1eac77eee/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccb9052771216170015f810b88065fd9e13b1e0b391f92abb9b47e0919a42aad", size = 210006, upload-time = "2026-07-06T15:26:25.837Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/28697000620e117eb413424caaf60b6f98ddb1b09b2c11f7c0038d9936a7/charset_normalizer-3.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3809ba5d3cd02aca0894597f2669a825bdfe2229061515c128b0f4e5533b4ab5", size = 225771, upload-time = "2026-07-06T15:26:27.079Z" }, + { url = "https://files.pythonhosted.org/packages/12/a7/d5844e315f5f35e7938c415f07a1df144eed1cf993f1b43cc16c980c5b46/charset_normalizer-3.4.8-cp312-cp312-win32.whl", hash = "sha256:de63c31666a049f653ada24e800192e3c019e96bc7d70fb449a000bccf26a36f", size = 150922, upload-time = "2026-07-06T15:26:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/9b/82/eb8b72f184b1e4986dd9daec15d7f6d9285a6728d2b07b7f04656829f473/charset_normalizer-3.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:14a4bbe066f3fb05c6ba70e9cf9d34614b57a2fd70ea8c27cc30f34155e16a58", size = 162294, upload-time = "2026-07-06T15:26:29.745Z" }, + { url = "https://files.pythonhosted.org/packages/09/5a/ab810134aa41034a08ffe94c058102016e6ad9bce62f3cdba547b4723385/charset_normalizer-3.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:2b5b0c0dca0a02c3f816f89abf18af3d20416dedbc3d3aa5f3981045f88ae7b0", size = 152409, upload-time = "2026-07-06T15:26:31.034Z" }, + { url = "https://files.pythonhosted.org/packages/11/49/fe5a8572a70cd9cba79f80af9388ac8c5c914ed4459b956f940244e499a5/charset_normalizer-3.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:057f8609f7341618c98e5aa9a6109fa116acff2a658497d47ab3325b5e8f2b08", size = 317424, upload-time = "2026-07-06T15:26:32.23Z" }, + { url = "https://files.pythonhosted.org/packages/e5/59/d71c96616b6825425a876f79f38fa440db30b32cc1166179a839f6259150/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c591f9a82adc5b89a039b90df74e43de2b9177fb46771172bed7b80722a70db0", size = 214723, upload-time = "2026-07-06T15:26:33.635Z" }, + { url = "https://files.pythonhosted.org/packages/c6/35/dc9eeb297f19b7b6ada39709ccb74937e6c51f0947958ae601a977cedd5d/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b56f449132d9adefe55b87635d05177a914ed5d070438a74725e1d77a280002", size = 236200, upload-time = "2026-07-06T15:26:34.99Z" }, + { url = "https://files.pythonhosted.org/packages/0a/37/6775fe852b4acad8bf7e0575fbe8aa9f41b546e33251acbded3c04a6b0d9/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1004c3b5831a301dadfb9e916f38e78e2ff3e08db24a1ad7c354db8ee3dea9c3", size = 231740, upload-time = "2026-07-06T15:26:36.498Z" }, + { url = "https://files.pythonhosted.org/packages/e4/28/1bcc3f5f3bac81532384adcfcdd9362c7f46a188a19deacc1ddaf7bdaa00/charset_normalizer-3.4.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60883e22821d17c9e5b4f3ca1ef8074f766e3db28791f851b665929c515635c0", size = 222888, upload-time = "2026-07-06T15:26:38.161Z" }, + { url = "https://files.pythonhosted.org/packages/47/81/9f3993ca62ef090c58059da641e49e3129e74700a6a3beb58436cdb8d4b9/charset_normalizer-3.4.8-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:21e4dbb942c8a6342e2685f232dd2a7bc73465697bd26ead4f118271d28be383", size = 207649, upload-time = "2026-07-06T15:26:39.628Z" }, + { url = "https://files.pythonhosted.org/packages/11/f4/679f636bcbdc2d53d06b1f4039be310450dca95a9f76bbf22f09985556e8/charset_normalizer-3.4.8-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84bcae14c65e645ca66b661339183d32b8c846a17c96e3e81ab3d346e1c498d4", size = 218908, upload-time = "2026-07-06T15:26:40.911Z" }, + { url = "https://files.pythonhosted.org/packages/7e/44/96e8c81867ba8a45ff893c8e7474c2d6b9633f7aa663da7901d040214d3e/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4e4d44b8287aa13a25e16e29393d494b0643b24894f7c8266c6f6788dd36337", size = 217096, upload-time = "2026-07-06T15:26:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/a5/bf/4d53f04f29bdb22601701f4f9f4d038edfb27976c296fcb7400c02736a6e/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f68545d1b267dbfafd5d253b6d1cb161562c4e61ab25b5c4cdb7d9e5923e441e", size = 209355, upload-time = "2026-07-06T15:26:43.557Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/92b3f630798d777fa880ad289a3f9f2fc663e4b4beb24783c53318820254/charset_normalizer-3.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1936e48214adea74922a20c8ab41b1393ae27cc9e329eb1f0b937d3416824f36", size = 224732, upload-time = "2026-07-06T15:26:44.892Z" }, + { url = "https://files.pythonhosted.org/packages/bd/85/eafa0a3c7bb6fe9f02f4c7901f02071933cac85ee634197e17280818c6de/charset_normalizer-3.4.8-cp313-cp313-win32.whl", hash = "sha256:1f8e3521860187d597f3867d8466da225b9179ea2833bb26de1bb026144d07c3", size = 150358, upload-time = "2026-07-06T15:26:46.153Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8c/879fafff7b47bb1166d289f2d2472cb31b9922f9f4ca1f392edf85ec16be/charset_normalizer-3.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:8b654b6f52a0a9a6be38e88f3e1dc68f1093ebeb2abbadafc7c82da0786a34be", size = 161685, upload-time = "2026-07-06T15:26:47.423Z" }, + { url = "https://files.pythonhosted.org/packages/c3/46/b57c7e778a7b578f28d35fd38544687d4f8d9c019585eebc5ad936073fad/charset_normalizer-3.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:d2d5a250ee26e29468b7607d97479221b069fa8aaf6f929ac84ec0e962e15154", size = 152333, upload-time = "2026-07-06T15:26:48.689Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/0a8540b8cd494951cca1428606373942803f5ffcec40fe798f819c5a8adb/charset_normalizer-3.4.8-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:77e993ecf65f21ab1f82266ff5e84a7de2c879e7d9b8bc006009df83f22a1d5e", size = 316993, upload-time = "2026-07-06T15:26:49.962Z" }, + { url = "https://files.pythonhosted.org/packages/0e/99/a0868f0a1f0a045fd374d1f2cf7042d8ad5d7fb4dd1f4ac7365e319f7e32/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:524939917f17f6de502dfda30b472550965740d7f126659d4c4f8dd1569cce22", size = 215638, upload-time = "2026-07-06T15:26:51.338Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e9/43c4d09a09b5557cc5fe1d87c9d96f86a3942aec0517d2b5408cef87ca75/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a4508989ba8e2ce43ef989453d18188b261546e8188cbdd4ef451fb9e4c3b467", size = 236456, upload-time = "2026-07-06T15:26:52.531Z" }, + { url = "https://files.pythonhosted.org/packages/e2/67/492ca98b3ab785b736b5da10c1bc233e1c8fec6c0cdb29b482c38bfc52a2/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e44127f7d11eee4548ad2cdf1f4e1b6eaaddd5cb92d15ad65f6ecc9bcf403ab", size = 232253, upload-time = "2026-07-06T15:26:53.838Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/1e6eff58c14f1aace1e26d80defbeaea2d35e075dbe4b611111ee4b47fa8/charset_normalizer-3.4.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb90317359f7e67bb6df615999a95e0980877468e617ddce8b6c2f8e7fe60d95", size = 222886, upload-time = "2026-07-06T15:26:55.009Z" }, + { url = "https://files.pythonhosted.org/packages/40/7a/90056a5326b0c4b9a3f924d337729c344c11542e5bc7191e50410db61587/charset_normalizer-3.4.8-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:35d9e7a9c960520ae89d1f4e305d1c047a74dea2e0f73a0e84f879356c2e8776", size = 206482, upload-time = "2026-07-06T15:26:56.306Z" }, + { url = "https://files.pythonhosted.org/packages/18/ff/94761d31a33878dbb5008ddbd918615061fcf5c0a612aa3075450e60f628/charset_normalizer-3.4.8-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:92e322b054c7ff886f78feab7360736bb45de2e18cf4a0ee84e8fc5a08d53a19", size = 218929, upload-time = "2026-07-06T15:26:57.422Z" }, + { url = "https://files.pythonhosted.org/packages/2b/dc/00b9675acd7c4b926b9102ee3f0d1a570ce943901be73b87485001393fe1/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c0086d97094363556206dc3bcf43f7edcfc043ea7a568a46f45efea74858bd1", size = 218069, upload-time = "2026-07-06T15:26:58.719Z" }, + { url = "https://files.pythonhosted.org/packages/04/11/94ada5a0482ee4bf688d04be4c7d6fd945d37370d04a95671040dfe2b416/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0752c849b51198267df2aba013c4de3a2955bd014a4fd70828809946c1acbc0c", size = 207146, upload-time = "2026-07-06T15:27:00.058Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f7/246bd36762207ab4752cd436b64e5d81a1668b15ddea7b5b2d0e8545e727/charset_normalizer-3.4.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2a4707e09eca11e81ece4fced600c5a0a801f568b962244f6f517bc274745fc9", size = 224896, upload-time = "2026-07-06T15:27:01.599Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f7/3510622d1fbe13b0ebf827c475e40a27e2be427140d792878b63ab6425cc/charset_normalizer-3.4.8-cp314-cp314-win32.whl", hash = "sha256:8ea67f427c073ae3da0923aa55f3715131fa613a61a7f2f8d762bde75eaf00ae", size = 150851, upload-time = "2026-07-06T15:27:02.964Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/9ce65dd21672b55cf800cca5f4433afa1586fda1d78731067ec9ec544c62/charset_normalizer-3.4.8-cp314-cp314-win_amd64.whl", hash = "sha256:ff71018850863362e5c7533769d0a9f77715c31af1502d523630ce822922f5c9", size = 162549, upload-time = "2026-07-06T15:27:04.249Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/9a5967eed666a88f31a0866884606d9ec3c2cd6091e2ccd7e0b4c4176c35/charset_normalizer-3.4.8-cp314-cp314-win_arm64.whl", hash = "sha256:44464e66f4da2f21dea7145c7693f9f60717ca4794a954dea5bf8c2c932678bd", size = 153079, upload-time = "2026-07-06T15:27:05.608Z" }, + { url = "https://files.pythonhosted.org/packages/02/4f/aa44cc81d8987f105352c74c0bf919007f8b80e9880d28bcf0393c1a816e/charset_normalizer-3.4.8-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:50a0c2e58ad2c203adb616fef28941b7e13716adbc25e0dfaeec29f5afe6382f", size = 338586, upload-time = "2026-07-06T15:27:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2b/b0392e2b235c08ff0623d905c2ee8ac820620544043c1ce92ce0b3d64c55/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1e589fdb95c76f08288bbb346230cdd8994db74903db6637b380f7b5fc9336", size = 222764, upload-time = "2026-07-06T15:27:08.23Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a1/7d466879190731f5559662c22232646f2ae2dace2323c3e5aefcf78d458a/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3d7c887444c5a7ef0d68d358d81e758a850bc626f8e639e2ca5667153272b20", size = 241331, upload-time = "2026-07-06T15:27:09.512Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/8b89e797137aa28c8fb0bafbafc243246a7afe21620a13b00e37624ece1d/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:65c389b96c0cfff3a3f0458fa1c7ce554a30e23101a88a49f03997afce6a929f", size = 239323, upload-time = "2026-07-06T15:27:10.86Z" }, + { url = "https://files.pythonhosted.org/packages/d7/98/1c1940730ed22d50983be4e243c722c89d5136d6f073bd840d1128bfddcb/charset_normalizer-3.4.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:593403fc47dcdf55e2987b2e3cc2e064127e2b908929f1f18b2e4a4652cbd780", size = 229964, upload-time = "2026-07-06T15:27:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/53/2d/bb8e81b7ff603d3f77e9a8a5d1ad34fcabbf3c54d300c29d99fba581fa23/charset_normalizer-3.4.8-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:606088e9fa2b7469ab9c42d4da8e05a415622a07714edd2fcd8fed48dda4c853", size = 212405, upload-time = "2026-07-06T15:27:13.447Z" }, + { url = "https://files.pythonhosted.org/packages/ce/1f/e52a3a53b13da591bb8f21d29e63877268eadf20686b7762351d4b89062c/charset_normalizer-3.4.8-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0317406326fed512f42a1632ad91a96228a7616c06547666a6dd79967f1bd6ca", size = 226918, upload-time = "2026-07-06T15:27:14.89Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f9/32996d79c57189af9722fe618f46d8a86b7be035ca98887b8d0c3821f141/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b67d50ee47e5c57a0064a9cb575b963a7125819dfd1fd094d44d378fff94659b", size = 225113, upload-time = "2026-07-06T15:27:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d2/9248c18e695696513774523a794cfb8b677521ce9ad7554d301cb10a9b20/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79e402b869f270140afa5e2b0e2ac100585358d812fe3dd093d424f7a72964e0", size = 214966, upload-time = "2026-07-06T15:27:17.418Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9d/4b19432d406179a40f924691906ee5b15ac664b408971c973295192444ea/charset_normalizer-3.4.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2970b9f7ab69ec3a0423ec6b6ac718e79fbf4a282c0bc103ef88c1ef50dfa15a", size = 231699, upload-time = "2026-07-06T15:27:19.131Z" }, + { url = "https://files.pythonhosted.org/packages/be/41/bdbdf71e8c3ccff10ef3cc2bb9467a7fdb3dc94b9a406d1a3c44afd39632/charset_normalizer-3.4.8-cp314-cp314t-win32.whl", hash = "sha256:458c2972a78043b7261c9726670029f15f722e70669bcbe961153a01968f589f", size = 155333, upload-time = "2026-07-06T15:27:20.681Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f8/e05c69323bd50091ec39f5f885385b884624b0131a6885a0c83a6217ba7a/charset_normalizer-3.4.8-cp314-cp314t-win_amd64.whl", hash = "sha256:0c926329a1df7cd56d7d8349fe354460d20aefd2e394c9e159e479d018b2b359", size = 167378, upload-time = "2026-07-06T15:27:22.042Z" }, + { url = "https://files.pythonhosted.org/packages/c2/04/cbaf1a2f5e2bbf70760e774380cbf052b10849fc35e770905df31af5cf00/charset_normalizer-3.4.8-cp314-cp314t-win_arm64.whl", hash = "sha256:2232baea80a2b01783679fed4e625ccdb19a974f44c9cf0fba21a777a4c8179c", size = 157782, upload-time = "2026-07-06T15:27:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/23/52/d5bee5b6ea81882d549b566d2545b044bbcbc33fe5fbe001008a7e745a21/charset_normalizer-3.4.8-py3-none-any.whl", hash = "sha256:b7c1fb310df524e01fbe84d43b7f95aa4f808f8eaa0dafc185f64ba395e37d54", size = 64279, upload-time = "2026-07-06T15:27:57.043Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, +] + +[[package]] +name = "click-log" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/32/228be4f971e4bd556c33d52a22682bfe318ffe57a1ddb7a546f347a90260/click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975", size = 9985, upload-time = "2022-03-13T11:10:15.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/5a/4f025bc751087833686892e17e7564828e409c43b632878afeae554870cd/click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756", size = 4273, upload-time = "2022-03-13T11:10:17.594Z" }, +] + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, +] + +[[package]] +name = "click-repl" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, +] + +[[package]] +name = "code-annotations" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, + { name = "python-slugify" }, + { name = "pyyaml" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/cd/73b009a7790d67d218c7caf5f1030f37f6f4863383c3dee47f44eac7a568/code_annotations-3.0.0.tar.gz", hash = "sha256:7ce2e883ab5c62b27fd3e490658f9ccbd632910422b80280d659b851483b0200", size = 44676, upload-time = "2026-03-13T16:36:41.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/4e/fc55066be96e4efbd73b52434d438dffb62ed57b6b2125abae41b7d624bc/code_annotations-3.0.0-py2.py3-none-any.whl", hash = "sha256:5470320f207bc71023989484fe60c9b45e5122c7c6d3666d7dd3a98aa7434864", size = 45424, upload-time = "2026-03-13T16:36:40.427Z" }, +] + +[[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.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/8b/adeb62ea8951f13c4c7fef2e7a85e1a06b499c8d8237ea589d496029e53f/coverage-7.15.0.tar.gz", hash = "sha256:9ac3fe7a1435986463eaa8ee253ae2f2a268709ba4ae5c7dd1f52a05391ad78f", size = 925362, upload-time = "2026-07-02T13:10:50.535Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/74/fd4c0901137c4f8d81a76ada99e43c65163b4c94a02ece107a4ec0c6b615/coverage-7.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b75ee5e8cb7575636ac598719b4307ac529ec8fcd79608a35c3cd4d4dada812d", size = 220838, upload-time = "2026-07-02T13:09:02.084Z" }, + { url = "https://files.pythonhosted.org/packages/0f/2e/2347583467bd7f0402635101a916961915cc68fce652cd0db5f173ea04fc/coverage-7.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffb31267816b93b075302248cc1737506081b4f163df4401e9df1a6424aafabe", size = 221197, upload-time = "2026-07-02T13:09:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/f0/17/99fa688541ae1d6e84543a0e544f83de0c944815b63e9e7b1ed411d15036/coverage-7.15.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e4d0bb73455bf97ab243a8f12c37c686ccf1c13bb614b7b85f1d062f06f42b2c", size = 252705, upload-time = "2026-07-02T13:09:05.059Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/6a95a5cd83b74839017ef9cf48d2d8c9ae60af919e17a3f336e6f9f1b7bd/coverage-7.15.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:20d9ccc4ebd0edc434d86dfd2a1dd2a8efa6b6b3073d0485a394fee86459ebb4", size = 255441, upload-time = "2026-07-02T13:09:06.559Z" }, + { url = "https://files.pythonhosted.org/packages/67/f2/406f6c57d600f68185942422c4c00f1a3255d60aee6e5fd961425cd9987e/coverage-7.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20c8a976c365c8cb12f0cbd099508772ea41fb5fa80657a8506df0e11bd278c5", size = 256556, upload-time = "2026-07-02T13:09:08.197Z" }, + { url = "https://files.pythonhosted.org/packages/74/8e/d3fa48489c15ecdec1ba48fd61f68798555dddd2f6716f9ad42adeb1a2a9/coverage-7.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f948fd5ba1b9cbca91f0ae08b4c1ce2b139509149a435e2585d056d57d70bf01", size = 258815, upload-time = "2026-07-02T13:09:09.691Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/2d40ddd110462c6a2769677cf7f1c119a52b45f568978fc6c98e4cc0dd0f/coverage-7.15.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f58185f06edf6ad68ec9fb155d63ef650c82f3fbd7e1770e2867751fb13158f4", size = 253117, upload-time = "2026-07-02T13:09:11.212Z" }, + { url = "https://files.pythonhosted.org/packages/51/c0/310782f0d7c3cb2b5ac05ba8d205fe91f24a36f6bf3256098f1782181c38/coverage-7.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02adc79a920c73c647c5d117f55747df7f2de94571884758ce8bc58e04f0a796", size = 254475, upload-time = "2026-07-02T13:09:13.029Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/702da6c275f8ae6ade423d2877243122932c9b27f5403003b9ef8c927d12/coverage-7.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6eb7c300fbed667fd6e3588eba71c1904cdb06110ca6fdf908c26bdd88b8e382", size = 252619, upload-time = "2026-07-02T13:09:14.699Z" }, + { url = "https://files.pythonhosted.org/packages/fb/84/c5b15a7e5ecba4e56218d772d99fe80a63e63f8d11f12783723a6005ab45/coverage-7.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b5fb23fa2de9dce1f5c36c09066d8fcda16cd96e8e26686caa2d7cb9b567d65c", size = 256689, upload-time = "2026-07-02T13:09:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/95/2f/c8b07559b57701230c61b23a953858c052890c12ef568d81780c6c46e92e/coverage-7.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cec79341dbe6281484024979976d0c7f22beae08b4a254655decd25d42cbe766", size = 252189, upload-time = "2026-07-02T13:09:17.828Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/6d2f049dd3fd3dbfd60b62ba6b2162a04009e2c002ce70b24cf3878dec7a/coverage-7.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c664c5444b1d970b1b2a450e21fb19ee5c9cfdf151ded2dda37260031cca0da", size = 254059, upload-time = "2026-07-02T13:09:19.304Z" }, + { url = "https://files.pythonhosted.org/packages/ce/92/b0287a2c42031d25c628f815f89a3cd9f8268ee78bb1252c9356cda1c689/coverage-7.15.0-cp312-cp312-win32.whl", hash = "sha256:5f764a3fa339bde6b3aa97657f5a6a3a9451e4a5b4ea98a2892c773a43525f77", size = 222893, upload-time = "2026-07-02T13:09:20.812Z" }, + { url = "https://files.pythonhosted.org/packages/a9/69/e34c481915fecb499b3146975061dac528752e37706edc1804f32c822469/coverage-7.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:52f9a4d2c4c56c8848bc2f524916698354b0211488b38c49ad9ae54f6cafbff6", size = 223429, upload-time = "2026-07-02T13:09:22.315Z" }, + { url = "https://files.pythonhosted.org/packages/fe/98/6e878f0b571d32684ef3f38d7c03db241ca5b82a5da8a5391596a8f209c4/coverage-7.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:31e5c3e70c85307ea35a12964e2e40f56ca2ee4b1c8c721ccf4609d17071080b", size = 222810, upload-time = "2026-07-02T13:09:23.812Z" }, + { url = "https://files.pythonhosted.org/packages/76/04/145a3748098bcc86b631a85408d2c3dc5c104e0bd86d605468239b25b6c4/coverage-7.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5be4caf3b28836f078abe700f8944dac4a65d78f16d6c600c89cb624e5535782", size = 220863, upload-time = "2026-07-02T13:09:25.371Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/4ed55708fed2c64b63c9bc5715daef670872202101938869b7fe5d5fbb8f/coverage-7.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd58ad1404704303ca8d4f4b8a1095e7cbc7040ef17a66df1e6619aa10176430", size = 221230, upload-time = "2026-07-02T13:09:26.897Z" }, + { url = "https://files.pythonhosted.org/packages/7b/19/3a80b97d3b2a5c77a01ae359c6bed20c13738fe3d9380f08616d4fec0281/coverage-7.15.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bbcbb317c2e5ded5b21104af81c29f391be2af98d065693ffbe8d23949b948e5", size = 252227, upload-time = "2026-07-02T13:09:28.543Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/b70062750686bd7da454da27927622f48bbac6990ac7a4c4a4653e7b0036/coverage-7.15.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27f31ecb458da3f859aab3f15ada871eb7a7768807d88df4a9f186bb17737970", size = 254823, upload-time = "2026-07-02T13:09:30.177Z" }, + { url = "https://files.pythonhosted.org/packages/a9/09/dad6a75a2e561b9dc5086a8c5257a7591d584246f67e23e70d2995b89ab6/coverage-7.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fb759be317fdc62e0f56bffdf61cfcb45c7761ad6b71e3e583e71a67ae753c", size = 256059, upload-time = "2026-07-02T13:09:31.979Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e7/b5d2941fa9564573d44b693a871ff3156f0c42cbefe977a09fa7fdc59971/coverage-7.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5cf007add5ab4bb8fa9f4c77e3732127c9e6cad501d7db43355fbfafca0be84", size = 258190, upload-time = "2026-07-02T13:09:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/7c/1d/8e895bcde3c57ccd46d896dda5f2b3d5df761a1b0c6c9d450d175dedc632/coverage-7.15.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc78d9843bd576fbe2118248258d485e968dc535f95ed504a7b0867ba9b51389", size = 252456, upload-time = "2026-07-02T13:09:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/f6997da343ddeb959be82c3b05322793f92c071ad45f7cb8a96336e2dd5f/coverage-7.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a263060f1de0b4b74b4e089c2a70b8003b3781c733329a9c8fd54995328f9950", size = 254192, upload-time = "2026-07-02T13:09:37.445Z" }, + { url = "https://files.pythonhosted.org/packages/17/27/a0bc09d032267b9da89d95a2d874cfbef2a5aebbf0e87cf7aba221d79a99/coverage-7.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c48decf16e0dfd5b049c7d5e82200c23c08126719142998d4f172444e3d0529e", size = 252153, upload-time = "2026-07-02T13:09:39.422Z" }, + { url = "https://files.pythonhosted.org/packages/54/c0/77fc233d9fba07b244c40948c53fe27308b8f21732fb3417f87fbd6fd992/coverage-7.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:08fb028000ed0aaa0a4cbdfbb98be7cb42f370db973fbbb469733505ab20e13e", size = 256310, upload-time = "2026-07-02T13:09:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/d5/24/601cecfb5825becacb8d45219a018a3b55b9dbaec624efdb0ea249d08be2/coverage-7.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb7dc0c3b7d8a1077abea0b8546ebc5e26d6ef6ecefc2f0f5ad2b8a53bdad837", size = 251974, upload-time = "2026-07-02T13:09:42.733Z" }, + { url = "https://files.pythonhosted.org/packages/47/1e/6f45e5a5b3d5484318d368702af6716b5ab8913b0428bec981a562fcf296/coverage-7.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cb3602054ccbe9f0d8c2dc04bbeba90d5719236e2cd06e042ddd6d3fc7b6e37", size = 253745, upload-time = "2026-07-02T13:09:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/8e/db/4df027a77bd11d0e527f44c53557c76e54ad027413d0304252ea3a78d67e/coverage-7.15.0-cp313-cp313-win32.whl", hash = "sha256:0bf781da64326b677be344df505171435b6f58716108606621d5d27d964fff8b", size = 222902, upload-time = "2026-07-02T13:09:46.122Z" }, + { url = "https://files.pythonhosted.org/packages/a0/10/0355894d34e231f2c5449e71287e81a50793a325df2e2b027b7bcd9dfd19/coverage-7.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:2c57a275078ee3fa185f83e400f765bc764a549de66d99b47881645cbd4ea629", size = 223444, upload-time = "2026-07-02T13:09:47.687Z" }, + { url = "https://files.pythonhosted.org/packages/06/ef/bb725f263befaaff851203ab338e68af15e195d7f7b5f323162532d9b6a8/coverage-7.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:3812c61afc6685c7999b39320779ab8f43b7a3081fdb0def39976e56fbdb9a21", size = 222839, upload-time = "2026-07-02T13:09:49.717Z" }, + { url = "https://files.pythonhosted.org/packages/4f/9c/1e3ca54f72a3185ece06c58d871099898c48f0ed6430d17b6ab75f0d180a/coverage-7.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:41cb79af843222e11da87127ad0ecbfa878abadd0f770a4a99391a27d3887324", size = 220906, upload-time = "2026-07-02T13:09:51.339Z" }, + { url = "https://files.pythonhosted.org/packages/09/37/f718613d83b274880382f6b67e78f3802549ae39b0b3e65ae5b5974df56e/coverage-7.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7d2008989ef8fe54188d3f3bfa2e3099b025af11e90a6a1b9e7dc433d04263d8", size = 221239, upload-time = "2026-07-02T13:09:53.138Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/22bae91e0b75445f68d365c7643ed0aa4880bbf77450ee74ca65bdae53a7/coverage-7.15.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:769e8ece11a596315ebf5aa7ec383aeeed016c091d2bf6363ffb996d41529092", size = 252286, upload-time = "2026-07-02T13:09:54.996Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/bec5e32aa508615d9d7a2790effb25fb4dc28606e995816afe400b25ece3/coverage-7.15.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65a6b6164ee5c39e2f3803f314292d6c61a607ba7fee253d1e03c42dc3903502", size = 254789, upload-time = "2026-07-02T13:09:56.678Z" }, + { url = "https://files.pythonhosted.org/packages/17/29/0e865435b4354e4a7c03b1b7920046d31d0a273d55decefea27e011cb9bf/coverage-7.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75128817f95a5c45bb01d65fd2d8b9cb54bbe03d81608fb70e3e14b437ad56c2", size = 256135, upload-time = "2026-07-02T13:09:58.343Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/33a870b58a13325d62fc0a6c8f01fa0ff667cef60c7498e2382a147dfa18/coverage-7.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9887bb428fe2d4cd4bee89bac1a6c9932f484afd5b36fbd4ff6ea5f825bb1f5e", size = 258449, upload-time = "2026-07-02T13:10:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/18/7b/6fffe596bf3ddba8462758d02c5dad730fd91055a6634aa2e4226229181a/coverage-7.15.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0bfc0be1f702042207a93a00523b1065ee1fe951e96edf311581c0bbc2e34888", size = 252313, upload-time = "2026-07-02T13:10:01.946Z" }, + { url = "https://files.pythonhosted.org/packages/58/1b/11468dd6c1676ab831a70cb9a8d4e198e8607fa0b7220ab918b73fe9bfbd/coverage-7.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f64627d55def5a43282d70e08396672692f77e4da610a5bb8bb4060b432b6859", size = 254142, upload-time = "2026-07-02T13:10:04.065Z" }, + { url = "https://files.pythonhosted.org/packages/79/41/29328e21d16b1b95092c30dd700e08cf915bd3734f836df8f3bdb0e8fa9f/coverage-7.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2c6f0fa473003905c6d5bac328ee4eba9fbea654f15bc24b8a3274b23363fa99", size = 252108, upload-time = "2026-07-02T13:10:06.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/de/05ccfb990439655b35afbfd8e0d13fe66677565a7d4eb38c3f5ef2635e1c/coverage-7.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2bcf9afaf064172c6ec3c58a325a9957ad1178c05dd934e25f253321776e0676", size = 256385, upload-time = "2026-07-02T13:10:08.141Z" }, + { url = "https://files.pythonhosted.org/packages/51/0e/486828a3d2695ea7a2609f17ff572f6b01905e608379440a11da4b8dffbe/coverage-7.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:baf06bc987115d6fb938d403f7eab684a057766c490367999a2b71a6883110c6", size = 251923, upload-time = "2026-07-02T13:10:10.179Z" }, + { url = "https://files.pythonhosted.org/packages/18/c7/03582b6715f078e5e558354c87616d945b9894cda2dace8e4009b17035e4/coverage-7.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0405f2ff97b1c4c0e782cb32e02f32369bcf2e6b618b591d67e1ea754575dfe", size = 253580, upload-time = "2026-07-02T13:10:12.052Z" }, + { url = "https://files.pythonhosted.org/packages/db/dc/9e578bbaf2ecb4959a81b7e7601ad8cca772cba2892e8d144cb749b4a71a/coverage-7.15.0-cp314-cp314-win32.whl", hash = "sha256:ab282853ed5fbd64bbb162f19cb8fcb7087187508a6374b4f9c34ec1577c4e8f", size = 223107, upload-time = "2026-07-02T13:10:13.994Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3e/c8c3b75d8dbe0e35f7b0cc3ff5e949fc59500f70b21d0398813f66740664/coverage-7.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bb3040e9f4bbe26fcb0cd7cc85ac63e630d3f3a9c74f027abf4caa27e706663", size = 223597, upload-time = "2026-07-02T13:10:15.906Z" }, + { url = "https://files.pythonhosted.org/packages/cd/bc/3cbc9fb036eb388519bccd521f783499c39b64256013fbc362782f196fe1/coverage-7.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:346771144d34f7fa84ec28386f78e0f31653f33cf35e19d253d5b35f9e8201da", size = 223020, upload-time = "2026-07-02T13:10:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/28/00/199c4a8d656dff63102577a056c0fce2ff6a79e40adac092fc986c49cbf1/coverage-7.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d34a010905fb6401324ba016b5da03d574967f7b21ce48ea41e66f0f1f95f641", size = 221638, upload-time = "2026-07-02T13:10:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/9d0092c96a3d3a26951ecc7020826aa57bcb1b119ca81acbba996884ab13/coverage-7.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bb25d825d885ca8036795dacfc3924d33091fc76d71ebc99420c6b79e77d96fa", size = 221903, upload-time = "2026-07-02T13:10:21.514Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b4/c0ca3028f42c9a08e51feb4561ef1192e5de99797cd1db5b04590c215bda/coverage-7.15.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:94c9686bfe8a9a6810297aecbd99beaa3445f9e8dc2f80b1382cca0d86b64461", size = 263267, upload-time = "2026-07-02T13:10:23.261Z" }, + { url = "https://files.pythonhosted.org/packages/5f/aa/a375e3846e5d3c013dc600b2a3231089055c73d77f5393dd2192a8d64da6/coverage-7.15.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9bd671c25f9d85f09d7ec481d0e43d5139f486c06a37139847a7ce569788af72", size = 265390, upload-time = "2026-07-02T13:10:25.152Z" }, + { url = "https://files.pythonhosted.org/packages/92/e1/5783cdabb797305e1c9e4809fea496d31834c51fa772514f73dc148bcfc9/coverage-7.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:110cbdf8d2e216577312cf06ccf85539c0e5a5420ef747e4a4719b5e483c88cd", size = 267811, upload-time = "2026-07-02T13:10:27.249Z" }, + { url = "https://files.pythonhosted.org/packages/85/31/96d8bbf58b8e9193bc8389574a91a0db48355ee98feb66aa6bf8d1b32eea/coverage-7.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c5d4619214f1d9993e7b00a8600d14614b7e9d84e89507460b126aa5e6559e5", size = 268928, upload-time = "2026-07-02T13:10:29.242Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7a/5294567e811a1cb7eda93140c628fa050d66189da28da320f93d1d815c73/coverage-7.15.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:781a704516e2d8346fbbd5be6c6f3412dd824785146528b3a01816f26c081007", size = 262378, upload-time = "2026-07-02T13:10:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/69/3f/3f48538421f899f28946f90a3d272136a4686e1abf461cc9249a783ee0f3/coverage-7.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd4a1b44bcb65ee29e947ac92bbee04956df3a6bfc6143641bb6cae7ede00fc9", size = 265263, upload-time = "2026-07-02T13:10:32.942Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d3/092df15efcab8a9c1467ee960eb8019bbad3f9300d115d89ea6195f369ff/coverage-7.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0e4950c9d6d3e39c64c991814ff315e2d0b9cb8152363594212c9e55208c0a8f", size = 262866, upload-time = "2026-07-02T13:10:35.104Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ab/0254d2b88665efb2c57ad368cc77ab5de3435bd8d5add4729c1b0e79431e/coverage-7.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:fe9c87ff42e5472d80d21704972e1f96e104a0a599d77c5e35db5a3c562e2571", size = 266599, upload-time = "2026-07-02T13:10:37.05Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/1cfa4023e489ce6fbc7be4a5d442dbc375edb4f4fda39a352cedb53263c2/coverage-7.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f00d5ae1dd2fe13fb8186e3e7d37bcbd8b25c0d764ff7d1b32cef9be058510a8", size = 261714, upload-time = "2026-07-02T13:10:38.966Z" }, + { url = "https://files.pythonhosted.org/packages/b7/eb/fee5c8665656be63f497418d410484637c438172568688e8ac92e06574e7/coverage-7.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:363ab38cc78b615f11c9cac3cf1d7eef950c18b9fdedfb9066f59461dcf84d68", size = 264025, upload-time = "2026-07-02T13:10:40.789Z" }, + { url = "https://files.pythonhosted.org/packages/ab/99/63005db722f91edc81abc16302f9cc2f6228c1679e46e15be9ae144b14d0/coverage-7.15.0-cp314-cp314t-win32.whl", hash = "sha256:54fd9c53a5fafff509195f1b6a3f9be615d8e8362a3629ff1de23d270c03c86b", size = 223413, upload-time = "2026-07-02T13:10:42.597Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e8/2bc6181c4fb06f1a6b981eb85330cc57bfad7e3f710fc9c9d350013ba228/coverage-7.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:87b47553097ba185ed964866078e7e63adea9f5f51b5f39691c34f30afd21080", size = 224245, upload-time = "2026-07-02T13:10:44.47Z" }, + { url = "https://files.pythonhosted.org/packages/79/b8/4d959bf9cc45d0cfed2f4d35cafcab978cdb6ea02eb5100009cd740632a3/coverage-7.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aeefb2dd178fe7eee79f0ad25d75855cb35ee9ed472db2c5ea06f5b4fd00cec5", size = 223558, upload-time = "2026-07-02T13:10:46.368Z" }, + { url = "https://files.pythonhosted.org/packages/52/30/21b2ad45959cd50e909e02ebac1e30b4ceb7162e91c11d4c570223a458b7/coverage-7.15.0-py3-none-any.whl", hash = "sha256:56da6a4cbe8f7e9e80bd072ca9cefe67d7106a440a7ec06519ec6507ac94ad19", size = 212632, upload-time = "2026-07-02T13:10:48.641Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "edx-lint" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "click-log" }, + { name = "code-annotations" }, + { name = "pylint" }, + { name = "pylint-celery" }, + { name = "pylint-django" }, + { name = "six" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/f0/834dcbb9d847d5654c1fc22dad18813eef66046d521e70d3d21be5a8647f/edx_lint-6.1.0.tar.gz", hash = "sha256:f879f55ed0a94222dee69a4361833f7de6b91576256881eeb35afbed2a85dfa2", size = 50843, upload-time = "2026-04-27T14:05:06.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/3c/dd55e7200bab2b60d993f76cf540b4098e02ce9d300367b395d936d48152/edx_lint-6.1.0-py3-none-any.whl", hash = "sha256:937eeee9c440bf4fbcbdee8553ee11d11a6c675e0834fe43f27174457de7c84a", size = 58548, upload-time = "2026-04-27T14:05:05.167Z" }, +] + +[[package]] +name = "face" +version = "26.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "boltons" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/fd/f84f0600bd72953d5a322f0dedbd4f900e2cedab718e6b6a093ae2d16aae/face-26.0.1.tar.gz", hash = "sha256:8183d94bc248baaea855a9f8445f97a22a9988908e60abddccc6e251da77c4c6", size = 51754, upload-time = "2026-06-17T23:14:39.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/24/0159c48d19c8b05e6969ee809bbbecc5a5c863f7e38c9327e2c63cb06f0f/face-26.0.1-py3-none-any.whl", hash = "sha256:ab0a83c37c9789dce658a67a9a80eafaa113c9ec37c5a9d950ff5480542a062d", size = 57572, upload-time = "2026-06-17T23:14:38.711Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/c8/35bdf04fb30755e2ed758f877edf3eb4a243c2463d3a258cc28b18b7a6e2/filelock-3.29.6.tar.gz", hash = "sha256:895c532ef3f4ef04972b9446a8c4e2931a5c399ff3c4be4c9369f2639b80f793", size = 70301, upload-time = "2026-07-06T23:08:08.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/49/7467c2946ccd9617f7da38187071bdc45bb9a95df51f4d63d6622432ce4e/filelock-3.29.6-py3-none-any.whl", hash = "sha256:14d5f5597d2e0c4dbd774cfb6d8132da1db44da83732aab679d54f7dcf97ab65", size = 45478, upload-time = "2026-07-06T23:08:07.197Z" }, +] + +[[package]] +name = "flask" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blinker" }, + { name = "click" }, + { name = "itsdangerous" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" }, +] + +[[package]] +name = "flask-sslify" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/98/54f2ffaf886d25eb1591cfb534c04cbf983236d657d58d180fd9ccbb5e7f/Flask-SSLify-0.1.5.tar.gz", hash = "sha256:d33e1d3c09cd95154176aa8a7319418e52129fc482dd56d8a8ad7c24500d543e", size = 3034, upload-time = "2015-04-02T20:25:24.407Z" } + +[[package]] +name = "freezegun" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/dd/23e2f4e357f8fd3bdff613c1fe4466d21bfb00a6177f238079b17f7b1c84/freezegun-1.5.5.tar.gz", hash = "sha256:ac7742a6cc6c25a2c35e9292dfd554b897b517d2dec26891a2e8debf205cb94a", size = 35914, upload-time = "2025-08-09T10:39:08.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/2e/b41d8a1a917d6581fc27a35d05561037b048e47df50f27f8ac9c7e27a710/freezegun-1.5.5-py3-none-any.whl", hash = "sha256:cd557f4a75cf074e84bc374249b9dd491eaeacd61376b9eb3c423282211619d2", size = 19266, upload-time = "2025-08-09T10:39:06.636Z" }, +] + +[[package]] +name = "github3-py" +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/91/603bcaf8cd1b3927de64bf56c3a8915f6653ea7281919140c5bcff2bfe7b/github3.py-4.0.1.tar.gz", hash = "sha256:30d571076753efc389edc7f9aaef338a4fcb24b54d8968d5f39b1342f45ddd36", size = 36214038, upload-time = "2023-04-26T17:56:37.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/2394d4fb542574678b0ba342daf734d4d811768da3c2ee0c84d509dcb26c/github3.py-4.0.1-py3-none-any.whl", hash = "sha256:a89af7de25650612d1da2f0609622bcdeb07ee8a45a1c06b2d16a05e4234e753", size = 151800, upload-time = "2023-04-26T17:56:25.015Z" }, +] + +[[package]] +name = "glom" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "boltons" }, + { name = "face" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/74/8387f95565ba7c30cd152a585b275ebb9a834d1d32782425c5d2fe0a102c/glom-25.12.0.tar.gz", hash = "sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069", size = 196128, upload-time = "2025-12-29T06:29:07.274Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/e6/4129d9a3baa72d747533bb33376543ccadd9a7f9944e5a6e3ae2e245f5d6/glom-25.12.0-py3-none-any.whl", hash = "sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1", size = 103295, upload-time = "2025-12-29T06:29:06.074Z" }, +] + +[[package]] +name = "gunicorn" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[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" }, +] +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 = "jira" +version = "3.10.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "packaging" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "requests-toolbelt" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/73/ee4daa7cf4eea457180de0ea78b730b44bb5ad2829dae49cf708a1460819/jira-3.10.5.tar.gz", hash = "sha256:2d09ae3bf4741a2787dd889dfea5926a5d509aac3b28ab3b98c098709e6ee72d", size = 105870, upload-time = "2025-07-28T12:18:22.796Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/57/ad078d7379e390798559446607e413fc953c7510711462ab34194dba5924/jira-3.10.5-py3-none-any.whl", hash = "sha256:d4da1385c924ee693d6cc9838e56a34e31d74f0d6899934ef35bbd0d2d33997f", size = 79250, upload-time = "2025-07-28T12:18:21.368Z" }, +] + +[[package]] +name = "kombu" +version = "5.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "amqp" }, + { name = "packaging" }, + { name = "tzdata" }, + { name = "vine" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, +] + +[package.optional-dependencies] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "librt" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/e0/dbd0f2a68a1c1a1991eb7921ff6014465d56608cdc9a9fb468a616210a37/librt-0.12.0.tar.gz", hash = "sha256:cb26faedbd09c6130e9c1b64d8000efec5076ffd18d606c6cd1cf02730e6d8b0", size = 203841, upload-time = "2026-06-30T16:14:29.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1a/5bec493821b0e85b91de4f234912b50133d1aedb875048eef27938ec3f96/librt-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9bce19aa7c05f91c989f9da7b567f81d21d57a2e6501e2b811aa0f3f79614c1a", size = 146756, upload-time = "2026-06-30T16:12:44.395Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d0/cc04b48a57c1f275387f5578847214c4a6c21bfb24c6c8c8d6ba753fe403/librt-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0ace09f5bf4d982fe726015f102fb856658b41580597104e301e630ed1d8d86", size = 145537, upload-time = "2026-06-30T16:12:45.95Z" }, + { url = "https://files.pythonhosted.org/packages/9e/10/c02325556beb2aa158c9e549ddade8cc9a23b36cdad14756dbed730c1ff1/librt-0.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d007efe9243ede81ce75990ad7aa172da1e2024144b3eff17ba46a5fff1fff3c", size = 488637, upload-time = "2026-06-30T16:12:47.658Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9e/7b49ca1c30baa9c8df96024aa09a97c35a97455e36004c9b5311703c56f3/librt-0.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ad324a5e4858388a4864915b90a42efc8b374376393f14b9940f2454e791912b", size = 483651, upload-time = "2026-06-30T16:12:49.283Z" }, + { url = "https://files.pythonhosted.org/packages/4d/71/03c8c8cec39645fda451132ff9d6d662fc5aea42a1a188a77a4fddb35906/librt-0.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10a40cf74cdd97b6f8f905056db73f5d459783de2ca04c6ebd1bf47652818e7e", size = 518359, upload-time = "2026-06-30T16:12:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ec/a9f357f94bbcba92277d22af22cff42ef706ae5d9d6d58b69bebf3a67954/librt-0.12.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:92e61c09de95217ae02a9d17f4f66cf073253cdc51bcfdc0f15c62c9a70baa85", size = 509510, upload-time = "2026-06-30T16:12:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/717055325d028743aa01a7691ad59a63352a26a8ff2e7eeb0c9249514150/librt-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0461344061d6fc3718940f5855d95647831cef6d03a6c7506897f98222784ad4", size = 527302, upload-time = "2026-06-30T16:12:54.244Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/7612eeedb3395d92f7c6a84dca5f15e282d650483a4dc01aa5b9cffdfda3/librt-0.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e6dfe89074732c9287b3c0f5a6af575c9ede380a788013876cc7b14fe0da0361", size = 532568, upload-time = "2026-06-30T16:12:55.74Z" }, + { url = "https://files.pythonhosted.org/packages/79/1e/a9afe85d5bb8b65dc27be3809ed1d69082079e1e9717fd2c66aa9939600c/librt-0.12.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9efed79d51ad1383bba0855f613cca7aa91c943e709af2413ac7f4bb9936ce08", size = 521579, upload-time = "2026-06-30T16:12:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/93aebb219d52c37ea578f83b0588cd7b040974e464d4e435086a48b4dc4d/librt-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1eac6cc0e23e448fb3c1446ed85ff796afb616eed5897c978d35dbec030b7c7c", size = 558743, upload-time = "2026-06-30T16:12:59.577Z" }, + { url = "https://files.pythonhosted.org/packages/3c/85/1680c0ec332f238e3145c5608d313ab0a43281e210a5dd87e3bc3cc25631/librt-0.12.0-cp312-cp312-win32.whl", hash = "sha256:0ab8ee0210047ae86ca023ccfbfe3df82077fd1c9bc021aebbf37d993ef64af0", size = 99200, upload-time = "2026-06-30T16:13:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/30/0e/abca12d8904875aa2ad66327390a3f7b1b75ebc43c0a00fc763cecf32ea5/librt-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:51c8bfa12632c81b94401c101bcedd0c56c3a1f8fa3273ca3472b28cd2f54003", size = 119390, upload-time = "2026-06-30T16:13:02.493Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/4203481b6d3a3bb348c82ac71abf1fcb4cb3ae8422a24a8dee4cd3ac5bd7/librt-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:5eebd451f5def089369ba6d8ff0291303d035e8154f9f26f7633835c5b029ade", size = 105117, upload-time = "2026-06-30T16:13:03.952Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/568d948c8079c9ff3c9e8110cf85f1eb70218e1209af29d0b7b89aa4a60c/librt-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d9a55760a34ae5ce70434aabb6a6c61c6c44a0ec58ca1cfd9cd86e4745d417d", size = 146808, upload-time = "2026-06-30T16:13:05.417Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/bea471ecea210088847bb5f3c4b4b424d596518934c06679b78ca85d6e63/librt-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ff0b197e338b4cf432873e0d6ef025213fdea85311ec4d87d2ea88c28adf2409", size = 145503, upload-time = "2026-06-30T16:13:07.023Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9e/984ad422b56de95fdce158f06b051655373784ebea0aba9a7fcbc41614d1/librt-0.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e69f120a20b69e2539d603bbd4d62db38399b10f8bf73a1cf445038a621e8af", size = 488421, upload-time = "2026-06-30T16:13:08.492Z" }, + { url = "https://files.pythonhosted.org/packages/50/03/1a2f94009b07ea71f8e1a4cfe53370565b56da9caa341b89e0699325e9f5/librt-0.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fde3cde595e947fc8e755b0a21f919a1622483d07c662d00496e040773d22591", size = 483488, upload-time = "2026-06-30T16:13:10.169Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3b/084bdc295823fbb6ab91670047adf8f420787f9e8794bf2d140b66dc196b/librt-0.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d977447315fa09ea4e8c7ae9b4e22f7659b5128161c1fd55ff786b5349f73503", size = 518428, upload-time = "2026-06-30T16:13:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/c9/22/5a307390b93a115ffbecd95c64eecb4e56269680e45e9415ada7285f2cf4/librt-0.12.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ffac8a67e4143cea9a549d4822b93bc0bbaad73fc25aa0ab0ba5ec27d178677", size = 509744, upload-time = "2026-06-30T16:13:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/90/83f3cb6184f5d669660717b4b2e317c9ddaccf7ca5bb97f2196deac1a3b7/librt-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:94af1ed773ff104ef08ef3d669a0ba9d3a5916c609eb698cffe5d5476d66ff9b", size = 527749, upload-time = "2026-06-30T16:13:15.277Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3b/f162be5cc88d47378e3a20776fe425fa1c2bece755da15e2783ebf06d3d6/librt-0.12.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:548199d21d22fb26398dfbbe0ba953a52465c66f3a49f38e6fddce1b127faf53", size = 532582, upload-time = "2026-06-30T16:13:17.074Z" }, + { url = "https://files.pythonhosted.org/packages/c9/28/6c5d2f6b7232fd24f284fc4cab37a459fe69a9096a09942f44cc5c55e073/librt-0.12.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c8f1f413b966a9dd3ecf80cd337b0ad7bb3de2474a4ff448ed3ebabfc3f803fc", size = 522235, upload-time = "2026-06-30T16:13:18.823Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1c/bd115360587fdc22c8ae8fac14c040a556b442e2965d4370d2cf274c8b95/librt-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55f13f95b629be5b6ab38918e439bf14169d6f9a8deaae55e0c14e12fb0c74b9", size = 559055, upload-time = "2026-06-30T16:13:20.509Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5a/c26f49f576437014825a86faea3cec60c1ed17f976abd567b6c12b8e35a7/librt-0.12.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:8b2dc079dfe29e77a47a19073d2040fa4879aa3656501f1650f8402ddce0313c", size = 79809, upload-time = "2026-06-30T16:13:22.401Z" }, + { url = "https://files.pythonhosted.org/packages/69/0b/a55244261d9ad7375ac039b8af06d42602722e2e8b8d8d6b86e4a3888c02/librt-0.12.0-cp313-cp313-win32.whl", hash = "sha256:da58944be8270f2bfee628a9a2a60c1cf6a12c8bea8e2c9b6edf3e5414ca7793", size = 99308, upload-time = "2026-06-30T16:13:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bf/ed9465e58d44c5a5637795547d0841c8934aab905ea452cac1adf14672cf/librt-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:1db4be3037e4ce065a071fa7deee93e78ebc25f448340a02a6c1c0b82c37e383", size = 119438, upload-time = "2026-06-30T16:13:25.188Z" }, + { url = "https://files.pythonhosted.org/packages/c0/44/3cad652aeb892e6e8ffe48d0fafa2bc652f28ec7ed3f4403fcbb1be4f948/librt-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:05fd2542892ad770b5dd45003fd080477cf220b611d3ee59b0792097eb0873a9", size = 105118, upload-time = "2026-06-30T16:13:26.533Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/3a0e05618c12423b6fc5141b590ec02a6efb645833edc8736a6c7b46d1ec/librt-0.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b37ee42e09722284a6d9288fe44a191f7276060a3195939bb77c6502058dbb34", size = 145579, upload-time = "2026-06-30T16:13:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/77/9e/fd399d099dfb4020f3f7c34e7e6210c389fa89f7d79ca92f5afb0395f278/librt-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ade11988728b3e4768dadc5696e82c60e9b35fc95335a9b4d1f5d69e753ccec7", size = 150139, upload-time = "2026-06-30T16:13:29.357Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ee/610239fbd8c4b005443664c5d4c3bc1717daedd8c71369bf45011aa87194/librt-0.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f351ed425380e39bd86df382578aa5b8c5b98e2e265112de7379e7d030258150", size = 480457, upload-time = "2026-06-30T16:13:30.78Z" }, + { url = "https://files.pythonhosted.org/packages/0c/10/ceddc9010f26c541444be36e1153a79b64626694db2d33a524c719fa3e46/librt-0.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:857d2163e088c868967717ace8e980017fd868a735f3de010412af02bdc30319", size = 479002, upload-time = "2026-06-30T16:13:32.398Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/b1523d9718e8192e5403e6b41a02742e17ba554369f0729b9f30ab590e2d/librt-0.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2befc80aa5f2f5b93f28abaaf11feff6677931dd548320e44c52deaa9399744", size = 510527, upload-time = "2026-06-30T16:13:34.615Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0e/0f3ff43befb18a531615736791e52fb67eaa71ff7b89e6e5f7004b64cc6e/librt-0.12.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:be3694dcfa97c6715dd19ac73d3e1b21a805514a5785663e57fecacd3ff64e5a", size = 500988, upload-time = "2026-06-30T16:13:36.408Z" }, + { url = "https://files.pythonhosted.org/packages/a8/1a/0278ea4a9e599dc507c43839a87f2c764ad04bf69418e2d763d58659e55f/librt-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2d5f67e86f45638843d025b0828f2e9e55fc45ff9180d2618ccdeaf72a796050", size = 519318, upload-time = "2026-06-30T16:13:37.883Z" }, + { url = "https://files.pythonhosted.org/packages/59/55/090e10e62be2f35265e41601337f83ac9f83be9aca1bf92692e3a82effdd/librt-0.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:64572c85e4ab7d572c9b72cd76b5f90b21181b1459fa6b1aac6f8958c4fcff31", size = 527127, upload-time = "2026-06-30T16:13:39.682Z" }, + { url = "https://files.pythonhosted.org/packages/1f/34/8052c9ec678be6ba751279947831f089aa69b009000b985ce91d1979669a/librt-0.12.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8b961912b0e688c1eb4658a46bdb0606b31918d65597fbe7356ca83aa653ffcc", size = 509766, upload-time = "2026-06-30T16:13:41.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f8/8761b36189e9ec8dc20b49fa84cef22852c6c41fcda56f760f7fc1360da5/librt-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:722375903e3f079436a7a33da51ce73931536dd041f9feb01536f05d8e010c96", size = 552043, upload-time = "2026-06-30T16:13:43.197Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/7283971ef6b70269938b49c7b25f670ec6325d252265fbcc996f9b364379/librt-0.12.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:a5a96a8f536b65ef1bf910c09e7e71647edde5111f6e1b51f413c6fba5bfe71b", size = 79472, upload-time = "2026-06-30T16:13:44.64Z" }, + { url = "https://files.pythonhosted.org/packages/c3/5e/b30940dea935e8ac5bd0e0abb1985f5274590d557ac3a252ca0d5392ce52/librt-0.12.0-cp314-cp314-win32.whl", hash = "sha256:8ffc99c356f1777c506e1b69dc303879153ae2640ba15b8f3d4448bc87139149", size = 94246, upload-time = "2026-06-30T16:13:45.962Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4e/0af9fe63f35fa304da3b05688f30ff6a329bcc59581b1cc51dc87fd30141/librt-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:1e68fb20798f455cda41d20a306a23c901218883f17a4bab1ed6e1331b265fb7", size = 114951, upload-time = "2026-06-30T16:13:47.279Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/843c495d7db35e13b84cd533898fa89145c40dc255da0bc316d53d631464/librt-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:2df534f97916cf38ec9b1ddafeb68ae1a4cd4a54775ff26a797026774c0517cf", size = 100562, upload-time = "2026-06-30T16:13:48.699Z" }, + { url = "https://files.pythonhosted.org/packages/75/30/c686d0f978d5fd6867c5bbad96b015c9445746764d1c228e16a2d30d9382/librt-0.12.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c09e581b1c2b8a62b809d4f4bd101ca3de93791e5b0ed1a14085d911be3dee3f", size = 153897, upload-time = "2026-06-30T16:13:50.017Z" }, + { url = "https://files.pythonhosted.org/packages/40/46/f6f2d77ce46628b48fb5280709013b5109cf3a2c46a2472093cdfc03519d/librt-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:976888d0d831402086e641018bcc3208e0a38f0835789da91f72894b2cb4161f", size = 156391, upload-time = "2026-06-30T16:13:51.462Z" }, + { url = "https://files.pythonhosted.org/packages/c2/46/cd790c7e19e460779471530ffab454541d6ea4a3b7d338cad7f16ff96995/librt-0.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:563c37cdb41d08fe1e3f08b201abac0e317ca18e88b91285466ee0a585797520", size = 564151, upload-time = "2026-06-30T16:13:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/724559a15fb023cbdef7aee1e81fbfbc3ee22fd09009baa816cea63e3a60/librt-0.12.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b97eb1a3140e279cc76f85b0fb92b7eb3dfbe0471260ee878bc9dc4bf9a0d649", size = 546002, upload-time = "2026-06-30T16:13:54.665Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7e/f9d8c257ab4909f101c7c13734367749e782fd8625545f0343502c2f09f1/librt-0.12.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06e0623351ab9904cf628245f99c714586f4dd23dc740b88c8bc670d8401a847", size = 584204, upload-time = "2026-06-30T16:13:56.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/33/64665810575ac23b6cb6ef364de51309b7803620c12885b6e895ebc29591/librt-0.12.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da12f017b2e404554be14d466cd992459feaa44f252b0f18d909a85266ce1237", size = 573688, upload-time = "2026-06-30T16:13:58.1Z" }, + { url = "https://files.pythonhosted.org/packages/0f/01/27522995c6627455abc7a939d57535fb1a7836d398ccedb3d7585f46039e/librt-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d97f31003a5c86b9e78155a829572c3a26484064fb7ac1d9695fe628bd93d029", size = 604719, upload-time = "2026-06-30T16:13:59.831Z" }, + { url = "https://files.pythonhosted.org/packages/ee/1f/099e61b1b688551d6d2ce9d4d2ae2242a938759db8551e6cbac7f7176ee5/librt-0.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:bd43a6c69876aef4f04eaae3d3b99b0be64755fda274002fa445b92480bf664e", size = 598183, upload-time = "2026-06-30T16:14:01.457Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c1/050400249665503bdd5b83cec518fa7b183b609341c8dcd58161775c4226/librt-0.12.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c01755c72fca1dc6b8d5c2ed228b8e7b2ffe184675c22f0f05ebd8fe188b9250", size = 582559, upload-time = "2026-06-30T16:14:03.29Z" }, + { url = "https://files.pythonhosted.org/packages/da/d1/eef8f0e6722518b65a3d3bcd9309f9f44e208ce5d6728070820f988e7078/librt-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:625ae561d5fa36400856dcc27464400d047bc2d5e3446be88f437b03fefd72e4", size = 626375, upload-time = "2026-06-30T16:14:04.957Z" }, + { url = "https://files.pythonhosted.org/packages/8b/78/f0bb41a6f2bbd3c77bdcc66980dc0d69ca1192a0ecec25377afcc5e6db73/librt-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8d73191883553ee0739741544bf3b00aba2a1224e45d9580b30cbc29e21dc03b", size = 97752, upload-time = "2026-06-30T16:14:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/92/24/e279c27972ab051a070237cfa45728fa51670c3f22f1a4d391711e9f4c31/librt-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e1cbb037324e759f0afa270229731ff0047772667f3cb38ef5df2cabf0175ede", size = 119562, upload-time = "2026-06-30T16:14:07.908Z" }, + { url = "https://files.pythonhosted.org/packages/06/e6/42a475bfca683b0cd5366f6dd06580062b7e567bb8534d225c877c2f14f3/librt-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bca1472acbd473eff61059b4409f802c5a1bcb4cd0344d06f939df9c4c125d40", size = 104282, upload-time = "2026-06-30T16:14:09.29Z" }, +] + +[[package]] +name = "logging-tree" +version = "1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/4d9bd637ecf05ec6c76179f91824b1a66425a9275abf676e87fe8a9c23a2/logging_tree-1.10.tar.gz", hash = "sha256:cd78848fe0ee4aafcc64fa8a66f96f177186ff3d883619b1d7f3628564802095", size = 12731, upload-time = "2024-05-03T16:05:18.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/1c/8e9c092e0faa0eb5ac8f5e859a62bd82239eba0259c05f6fd9bb6ddd6b91/logging_tree-1.10-py2.py3-none-any.whl", hash = "sha256:f0e9f4645e6476e48c563ba2b858286079f886b2d5ad8abeaffd38e536a387d8", size = 13280, upload-time = "2024-05-03T16:05:10.674Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +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/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" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, + { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, + { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, + { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, + { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, + { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, + { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, + { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, + { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, + { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, +] + +[[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 = "nh3" +version = "0.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", size = 24684, upload-time = "2026-06-22T00:47:02.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/3e/6506aa4f23dc7b7993a2d0a45dca3ce864ec48380adfe15a173e643c63e8/nh3-0.3.6-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2411e8c3cee81a1ddd62c2a5d50585c28aa5566d373ad1db92536b95ddb24ef2", size = 1421679, upload-time = "2026-06-22T00:46:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", size = 792570, upload-time = "2026-06-22T00:46:22.179Z" }, + { url = "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", size = 842243, upload-time = "2026-06-22T00:46:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", size = 1001468, upload-time = "2026-06-22T00:46:25.481Z" }, + { url = "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", size = 1082933, upload-time = "2026-06-22T00:46:27.15Z" }, + { url = "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", size = 1043120, upload-time = "2026-06-22T00:46:28.89Z" }, + { url = "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", size = 1023824, upload-time = "2026-06-22T00:46:30.453Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/1f7285ffadc8307c4dbeb08d21b920536d5117785056d1079e998c4dfa44/nh3-0.3.6-cp314-cp314t-win32.whl", hash = "sha256:597a8e843bea00b2eb5520658dc24a9bb032e7fc9e7c2c0c4cd29420220c9796", size = 599253, upload-time = "2026-06-22T00:46:32.072Z" }, + { url = "https://files.pythonhosted.org/packages/36/ea/5542f3c45da4c00290d9d67a65e996702e23e613c4b627de3e09cb9fe357/nh3-0.3.6-cp314-cp314t-win_amd64.whl", hash = "sha256:4713502748f564fee0633b37b3403783ce0a3af3a3d148ad91025a5bdadb7bc6", size = 612553, upload-time = "2026-06-22T00:46:33.53Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/26bd47e6af5915a628281dccdac354ddf4e32f7397047894270acd8c9870/nh3-0.3.6-cp314-cp314t-win_arm64.whl", hash = "sha256:69bbb92865a693d909db3a700d3c01537533844d0948c1e9323561ce06ecda41", size = 595151, upload-time = "2026-06-22T00:46:34.878Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ab/a7653bce9a3b204be6a6931767a9e23595807bb84790ce6685e4d7e5bd08/nh3-0.3.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a43ebd7543555c3ac1bc353023d0794e75cb76f6f18f19c32e95441496c0cc25", size = 1443564, upload-time = "2026-06-22T00:46:36.66Z" }, + { url = "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", size = 838002, upload-time = "2026-06-22T00:46:38.101Z" }, + { url = "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", size = 823045, upload-time = "2026-06-22T00:46:39.495Z" }, + { url = "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", size = 1093171, upload-time = "2026-06-22T00:46:41.21Z" }, + { url = "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", size = 1049217, upload-time = "2026-06-22T00:46:42.804Z" }, + { url = "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", size = 917372, upload-time = "2026-06-22T00:46:44.495Z" }, + { url = "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", size = 806699, upload-time = "2026-06-22T00:46:45.99Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", size = 835165, upload-time = "2026-06-22T00:46:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", size = 858282, upload-time = "2026-06-22T00:46:49.276Z" }, + { url = "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", size = 1014328, upload-time = "2026-06-22T00:46:51.026Z" }, + { url = "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", size = 1098207, upload-time = "2026-06-22T00:46:52.674Z" }, + { url = "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", size = 1056961, upload-time = "2026-06-22T00:46:54.335Z" }, + { url = "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", size = 1033829, upload-time = "2026-06-22T00:46:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/e9/55/9de666ad975d6ccd77d799ea0add55ee2347aa81286ce21b2a97c070746b/nh3-0.3.6-cp38-abi3-win32.whl", hash = "sha256:5276ef17bdba9ad8040575c74072008b13aae429436e9d0429e718bb5f90f4da", size = 609081, upload-time = "2026-06-22T00:46:57.665Z" }, + { url = "https://files.pythonhosted.org/packages/82/fa/2b5d684e3edf1e81bfd02d298c78c3e3da77ca1d8a2be3183a79544a7548/nh3-0.3.6-cp38-abi3-win_amd64.whl", hash = "sha256:f338ac7d594c067679f1e99b4f5ec3906842979560f9d8f15d6bdfa39a353b10", size = 624461, upload-time = "2026-06-22T00:46:59.163Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e5/7cafee2f0413ca4cb0ef3bd111e94d408a48810008b283ad8aee00dd1809/nh3-0.3.6-cp38-abi3-win_arm64.whl", hash = "sha256:69f365963f63a1e9bff53bdbb3c542c7c2efed3e163c9d5d83a772a2ac468c21", size = 603060, upload-time = "2026-06-22T00:47:00.596Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[package.optional-dependencies] +signedtoken = [ + { name = "cryptography" }, + { name = "pyjwt" }, +] + +[[package]] +name = "openedx-webhooks" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "arrow" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "celery", extra = ["redis"] }, + { name = "click" }, + { name = "cryptography" }, + { name = "flask" }, + { name = "flask-sslify" }, + { name = "github3-py" }, + { name = "glom" }, + { name = "gunicorn" }, + { name = "jira" }, + { name = "logging-tree" }, + { name = "oauthlib", extra = ["signedtoken"] }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "sentry-sdk", extra = ["flask"] }, + { name = "urlobject" }, +] + +[package.dev-dependencies] +ci = [ + { name = "tox" }, + { name = "tox-uv" }, +] +dev = [ + { name = "edx-lint" }, + { name = "freezegun" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-repeat" }, + { name = "python-dotenv" }, + { name = "pytz" }, + { name = "readme-renderer" }, + { name = "repo-tools-data-schema" }, + { name = "requests-mock" }, + { name = "ruff" }, + { name = "scriv" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinxcontrib-httpdomain" }, + { name = "types-cachetools" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +doc = [ + { name = "freezegun" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-repeat" }, + { name = "pytz" }, + { name = "readme-renderer" }, + { name = "repo-tools-data-schema" }, + { name = "requests-mock" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinxcontrib-httpdomain" }, +] +quality = [ + { name = "mypy" }, + { name = "ruff" }, + { name = "types-cachetools" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +test = [ + { name = "freezegun" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-repeat" }, + { name = "pytz" }, + { name = "repo-tools-data-schema" }, + { name = "requests-mock" }, +] +test-base = [ + { name = "freezegun" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-repeat" }, + { name = "pytz" }, + { name = "repo-tools-data-schema" }, + { name = "requests-mock" }, +] + +[package.metadata] +requires-dist = [ + { name = "arrow" }, + { name = "blinker" }, + { name = "cachetools" }, + { name = "celery", extras = ["redis"] }, + { name = "click" }, + { name = "cryptography" }, + { name = "flask" }, + { name = "flask-sslify" }, + { name = "github3-py" }, + { name = "glom" }, + { name = "gunicorn" }, + { name = "jira" }, + { name = "logging-tree" }, + { name = "oauthlib", extras = ["signedtoken"] }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "requests" }, + { name = "requests-oauthlib" }, + { name = "sentry-sdk", extras = ["flask"] }, + { name = "urlobject" }, +] + +[package.metadata.requires-dev] +ci = [ + { name = "tox" }, + { name = "tox-uv" }, +] +dev = [ + { name = "edx-lint" }, + { name = "freezegun" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-repeat" }, + { name = "python-dotenv" }, + { name = "pytz" }, + { name = "readme-renderer" }, + { name = "repo-tools-data-schema", git = "https://github.com/openedx/repo-tools-data-schema.git" }, + { name = "requests-mock" }, + { name = "ruff" }, + { name = "scriv" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinxcontrib-httpdomain" }, + { name = "types-cachetools" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +doc = [ + { name = "freezegun" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-repeat" }, + { name = "pytz" }, + { name = "readme-renderer" }, + { name = "repo-tools-data-schema", git = "https://github.com/openedx/repo-tools-data-schema.git" }, + { name = "requests-mock" }, + { name = "sphinx" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinxcontrib-httpdomain" }, +] +quality = [ + { name = "mypy" }, + { name = "ruff" }, + { name = "types-cachetools" }, + { name = "types-pyyaml" }, + { name = "types-requests" }, +] +test = [ + { name = "freezegun" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-repeat" }, + { name = "pytz" }, + { name = "repo-tools-data-schema", git = "https://github.com/openedx/repo-tools-data-schema.git" }, + { name = "requests-mock" }, +] +test-base = [ + { name = "freezegun" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-repeat" }, + { name = "pytz" }, + { name = "repo-tools-data-schema", git = "https://github.com/openedx/repo-tools-data-schema.git" }, + { name = "requests-mock" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pylint" +version = "4.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/1d/3bb57f303701549550d74bf7ced2b07412be97125c167a0c9d216aa9f762/pylint-4.0.6.tar.gz", hash = "sha256:52f19191bee08bf103f9705ad1a0ece4aa5a0a4ef2bdcbd969375a1e6f6579d5", size = 1585588, upload-time = "2026-06-14T14:43:26.772Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/da/acb2e7d4dbd2dfb792d38c0d850481f29ad7049b356d23f56c687d35203b/pylint-4.0.6-py3-none-any.whl", hash = "sha256:d11a0e1fdb7b1cd46ec5d6fc78fee8b95f28695b2d6140e5809925f61e32ea54", size = 538389, upload-time = "2026-06-14T14:43:24.873Z" }, +] + +[[package]] +name = "pylint-celery" +version = "0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "pylint" }, + { name = "pylint-plugin-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/11/7241fec486839a853cee783e3a00950d46ee1e3e993884341b1d4208a287/pylint-celery-0.3.tar.gz", hash = "sha256:41e32094e7408d15c044178ea828dd524beedbdbe6f83f712c5e35bde1de4beb", size = 1899, upload-time = "2014-09-25T07:23:50.824Z" } + +[[package]] +name = "pylint-django" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pylint" }, + { name = "pylint-plugin-utils" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/0d/d775fec0dde8ca5d20e9170a2ca332dfa21b77f7e7e47fc3ab9b2261773c/pylint_django-2.7.0-py3-none-any.whl", hash = "sha256:76ef7e7bbbcf7ee86adbb2beac0ffaa7232509a17bf4a488d81467a1bbaa215b", size = 42892, upload-time = "2026-01-01T11:17:04.292Z" }, +] + +[[package]] +name = "pylint-plugin-utils" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pylint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/85/24eaf5d0d078fc8799ae6d89faf326d6e4d27d862fc9a710a52ab07b7bb5/pylint_plugin_utils-0.9.0.tar.gz", hash = "sha256:5468d763878a18d5cc4db46eaffdda14313b043c962a263a7d78151b90132055", size = 10474, upload-time = "2025-06-24T07:14:00.534Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/c9/a3b871b0b590c49e38884af6dab58ab9711053bd5c39b8899b72e367b9f6/pylint_plugin_utils-0.9.0-py3-none-any.whl", hash = "sha256:16e9b84e5326ba893a319a0323fcc8b4bcc9c71fc654fcabba0605596c673818", size = 11129, upload-time = "2025-06-24T07:13:58.993Z" }, +] + +[[package]] +name = "pyproject-api" +version = "1.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/62/0fe346fe380b1aafaf819c8cb195d3241bb4f355f908e6339814131a830b/pyproject_api-1.10.1.tar.gz", hash = "sha256:c2b2726bd7aa9217b6c50b621fef5b2ae5def4d55b779c9e0694c15e0a8517ba", size = 23477, upload-time = "2026-05-28T14:22:14.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/d7/29e1e5e882f79133631f7bcace42d23db493f616463c157a1ab614bf69dd/pyproject_api-1.10.1-py3-none-any.whl", hash = "sha256:fa9e6f66c35b5017e909825d8f2b5d5482ea699d7be809d21c03bd1f7317f36a", size = 12992, upload-time = "2026-05-28T14:22:12.711Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "pytest-repeat" +version = "0.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/d4/69e9dbb9b8266df0b157c72be32083403c412990af15c7c15f7a3fd1b142/pytest_repeat-0.9.4.tar.gz", hash = "sha256:d92ac14dfaa6ffcfe6917e5d16f0c9bc82380c135b03c2a5f412d2637f224485", size = 6488, upload-time = "2025-04-07T14:59:53.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/d4/8b706b81b07b43081bd68a2c0359fe895b74bf664b20aca8005d2bb3be71/pytest_repeat-0.9.4-py3-none-any.whl", hash = "sha256:c1738b4e412a6f3b3b9e0b8b29fcd7a423e50f87381ad9307ef6f5a8601139f3", size = 4180, upload-time = "2025-04-07T14:59:51.492Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/26/8b004cc36f430345136f6f00fa1aa9ed596c8ed1e8504625fa79522ff39c/python_discovery-1.4.3.tar.gz", hash = "sha256:ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289", size = 70438, upload-time = "2026-07-03T13:21:51.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/78/9b77ecb4644d1bbea94d29abf78f21c47eca6eb79e9745b702ec0bed2e19/python_discovery-1.4.3-py3-none-any.whl", hash = "sha256:b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c", size = 33885, upload-time = "2026-07-03T13:21:50.174Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-slugify" +version = "8.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "text-unidecode" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, +] + +[[package]] +name = "pytz" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ff/46/dd499ec9038423421951e4fad73051febaa13d2df82b4064f87af8b8c0c3/pytz-2026.2.tar.gz", hash = "sha256:0e60b47b29f21574376f218fe21abc009894a2321ea16c6754f3cad6eb7cdd6a", size = 320861, upload-time = "2026-05-04T01:35:29.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/dd/96da98f892250475bdf2328112d7468abdd4acc7b902b6af23f4ed958ea0/pytz-2026.2-py2.py3-none-any.whl", hash = "sha256:04156e608bee23d3792fd45c94ae47fae1036688e75032eea2e3bf0323d1f126", size = 510141, upload-time = "2026-05-04T01:35:27.408Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "readme-renderer" +version = "45.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" }, +] + +[[package]] +name = "redis" +version = "6.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, +] + +[[package]] +name = "repo-tools-data-schema" +version = "2.0" +source = { git = "https://github.com/openedx/repo-tools-data-schema.git#b0a346fb41f8fb59c4037f5ad562c50bf4b708be" } +dependencies = [ + { name = "pyyaml" }, + { name = "schema" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-mock" +version = "1.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, +] + +[[package]] +name = "schema" +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/2e/8da627b65577a8f130fe9dfa88ce94fcb24b1f8b59e0fc763ee61abef8b8/schema-0.7.8.tar.gz", hash = "sha256:e86cc08edd6fe6e2522648f4e47e3a31920a76e82cce8937535422e310862ab5", size = 45540, upload-time = "2025-10-11T13:15:40.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/75/aad85817266ac5285c93391711d231ca63e9ae7d42cd3ca37549e24ebe52/schema-0.7.8-py2.py3-none-any.whl", hash = "sha256:00bd977fadc7d9521bf289850cd8a8aa5f4948f575476b8daaa5c1b57af2dce1", size = 19108, upload-time = "2025-10-11T17:13:07.323Z" }, +] + +[[package]] +name = "scriv" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "click" }, + { name = "click-log" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/9a/2ef2209e0672b264a2f2574dc88ea3cd9cfc9adfecbfd3165a900980ec8c/scriv-1.8.0.tar.gz", hash = "sha256:7b1a105dd411ac541998250fc8594742419f94cee984ca1257c5ebf5af21918b", size = 98160, upload-time = "2025-12-30T00:01:10.13Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/e7/062480ede84ecb56ee0f8f2e5b5a3b2a5bceeb73bbdf909d3c13f5438749/scriv-1.8.0-py3-none-any.whl", hash = "sha256:f00f51325b2f4bc96b16fbb1239d4ab577cc2422301a5dd4f5f9378aae2549e0", size = 39085, upload-time = "2025-12-30T00:01:08.599Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.64.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/31/b7341f156a5f6f36f0b4845d6f1c28a2ae4799171dba7007f3a1e9b234b4/sentry_sdk-2.64.0.tar.gz", hash = "sha256:68be2c29e14ae310f8a39e1a79916b6d85c6cb41dcce789d14ff05fe293e4c55", size = 921020, upload-time = "2026-06-30T08:13:47.682Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/a8/3fb9a4319efa3b26f5be0e90e6d8918df43fa7c7e977d26390f589501d82/sentry_sdk-2.64.0-py3-none-any.whl", hash = "sha256:715ea91ca860a819e8d8a50a7bde3a80d0df3b4ed7b6660a20fb9a2d084188f1", size = 498901, upload-time = "2026-06-30T08:13:45.566Z" }, +] + +[package.optional-dependencies] +flask = [ + { name = "blinker" }, + { name = "flask" }, + { name = "markupsafe" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/f8/0a71edf031f03c40db17503cb8ca78a69a171254e568e7db241b0ab57ea1/snowballstemmer-3.1.1.tar.gz", hash = "sha256:e07bbc54a0d798fe6010a12398422e62a8bfbba95c394fd0956ef58cb4d3e260", size = 123314, upload-time = "2026-06-03T00:56:40.194Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/07/2ebca9b11fb9be7340a818d8d6f63feaebb146be2c4afbd6061701d6df6e/snowballstemmer-3.1.1-py3-none-any.whl", hash = "sha256:7e207fa178741da09cdee59d3ecec3827ad5f92b1fc5c9ff3755b639f71f5752", size = 104164, upload-time = "2026-06-03T00:56:38.614Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-httpdomain" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/a2/b9b96904f691a0b4ccb8277a72b4ec351590286b44a433d0cefe78703c2b/sphinxcontrib_httpdomain-2.0.0.tar.gz", hash = "sha256:9e4e8733bf41ee4d9d5f9eb4dbf3cc2c22a665221ba42c5c3ae181b98af8855d", size = 17155, upload-time = "2026-02-04T21:23:56.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/48/9524b2a8cd11a3802a266aa4631ae7842cd764cf9bf3701bbde547b040b5/sphinxcontrib_httpdomain-2.0.0-py3-none-any.whl", hash = "sha256:e968775c9994f8139cb6ff91e1f6a8557396a2cc08073997eed10d9b39f96df3", size = 26137, upload-time = "2026-02-04T21:23:55.444Z" }, +] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, +] + +[[package]] +name = "stevedore" +version = "5.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/8d/008761f6e1000600e5303db30d05724bdcf3d2d186cbb59fac79b52e39ed/stevedore-5.9.0-py3-none-any.whl", hash = "sha256:e520945d4c257700eddc1eb1d79df04b2ea578eef185e0e3fa5b442fc848d3f7", size = 54463, upload-time = "2026-07-02T11:38:07.43Z" }, +] + +[[package]] +name = "text-unidecode" +version = "1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/db/03eaf4331631ef6b27d6e3c9b68c54dc6f0d63d87201fed600cc409307fd/tomlkit-0.15.0.tar.gz", hash = "sha256:7d1a9ecba3086638211b13814ea79c90dd54dd11993564376f3aa92271f5c7a3", size = 161875, upload-time = "2026-05-10T07:38:22.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/43/8bd850ee71a191bf072e31302c73a66be413fecdd98fdcd111ecbcce13ca/tomlkit-0.15.0-py3-none-any.whl", hash = "sha256:4dbc8f0fc024412b57ced8757ac7461305126a648ff8c2c807fcb8e133a78738", size = 41328, upload-time = "2026-05-10T07:38:23.517Z" }, +] + +[[package]] +name = "tox" +version = "4.56.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "colorama" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "pluggy" }, + { name = "pyproject-api" }, + { name = "python-discovery" }, + { name = "tomli-w" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/be/0a4370df64b4610c653482dd55e0a6818fa31c551fce7659c253e3668638/tox-4.56.2.tar.gz", hash = "sha256:346442f3982a5c1b792abcbc476a90f821031154251ea3506401c4c51c62cc2a", size = 286251, upload-time = "2026-07-07T02:11:16.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/66/34fdfb42ff381da15404bf8aa92b1b0e6ddad7263e4ec23bee3daf359d50/tox-4.56.2-py3-none-any.whl", hash = "sha256:245b60f2610e126070b289a862d6288401a387147e86a30c9316cb0ab276137b", size = 217329, upload-time = "2026-07-07T02:11:15.04Z" }, +] + +[[package]] +name = "tox-uv" +version = "1.35.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tox-uv-bare" }, + { name = "uv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/dc/6e9994c799bdbb309f829dd6b8d98764dd0757302f3433c380438a3a127b/tox_uv-1.35.2-py3-none-any.whl", hash = "sha256:2d99b0e3c782ba49e7cbe521c8d344758595961b17a3633738d67096641c1bde", size = 6565, upload-time = "2026-05-05T01:34:16.07Z" }, +] + +[[package]] +name = "tox-uv-bare" +version = "1.35.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "tox" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/cb/168dc1ccf24e4065a9a0a33df55709ed2b5eb73bd2b13ddd53187e5dffb8/tox_uv_bare-1.35.2.tar.gz", hash = "sha256:49e28a804c97f23ea17e25859960c0fa78f35bccb7e14344cfd840e89a9aade9", size = 32333, upload-time = "2026-05-05T01:34:18.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/4a33dc81da39db7b31e5622333df361e8fe055b7ec636bd5fea762c9182d/tox_uv_bare-1.35.2-py3-none-any.whl", hash = "sha256:c0d590a41d1054a1ad0874e9e5943ff52402786e3d4599d8f8d37a65b566ef53", size = 22307, upload-time = "2026-05-05T01:34:17.681Z" }, +] + +[[package]] +name = "types-cachetools" +version = "7.0.0.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/14/1e4fca2b250dbc75be9f0beab083acb3cd1151711e1031eb4a854dfd71be/types_cachetools-7.0.0.20260518.tar.gz", hash = "sha256:7730014e4fef0c6f01e2cd0f980f8ce6d1b1d2472c8459c1f382348ec1a6b435", size = 10072, upload-time = "2026-05-18T06:02:20.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/e0/767be6b60859fd2edc4512fabdedbce703fc8d4ec5007b31abaf37a51c6c/types_cachetools-7.0.0.20260518-py3-none-any.whl", hash = "sha256:997b356870915f8bbc9b2cdb4e7271c01d487996fdac2a9c8e91cc5b1261b3d1", size = 9500, upload-time = "2026-05-18T06:02:19.042Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260518" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/83/4a1afc3fbfcf5b8d46fc390cd95ed6b0dc9010a265f4e9f46314efffa37a/types_pyyaml-6.0.12.20260518.tar.gz", hash = "sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466", size = 17850, upload-time = "2026-05-18T06:01:58.675Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/a2/c01db32be2ae7d6a1689972f3c492b149ee4e164b12fdfd9f64b50888215/types_pyyaml-6.0.12.20260518-py3-none-any.whl", hash = "sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd", size = 20312, upload-time = "2026-05-18T06:01:57.368Z" }, +] + +[[package]] +name = "types-requests" +version = "2.33.0.20260518" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/01/c5a19253fe1ac159159ddf9a3a07cec8bb5e486ec4d9002ad2821da0e5d2/types_requests-2.33.0.20260518.tar.gz", hash = "sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e", size = 24752, upload-time = "2026-05-18T06:07:37.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/bc/b139710a3b6018f7fb2b9508b35c8af564e61bf2bf4fa619d088f3e16f85/types_requests-2.33.0.20260518-py3-none-any.whl", hash = "sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0", size = 21391, upload-time = "2026-05-18T06:07:37.044Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + +[[package]] +name = "tzlocal" +version = "5.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/5b/879b2f932adfa7a053c360d50bc896c977fa6426109185f7c12ebdd0cb9d/tzlocal-5.4.4.tar.gz", hash = "sha256:8dbb8660838688a7b6ba4fed31d18dedf842afb4d47ca050d6d891c2c15f3be4", size = 31170, upload-time = "2026-06-29T08:03:40.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a4/017a7a6cbe387d961a688ec31364ae60a5c4e22c96ae9921b79a947c855d/tzlocal-5.4.4-py3-none-any.whl", hash = "sha256:aae09f0126a8a86fa736be266eb4a471380d26a0de3bc14844e7821fee3e2a15", size = 18115, upload-time = "2026-06-29T08:03:38.666Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "urlobject" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/fd/163e6b835b9fabf9c3999f71c5f224daa9d68a38012cccd7ab2a2f861af9/urlobject-3.0.0.tar.gz", hash = "sha256:bfdfe70746d92a039a33e964959bb12cecd9807a434fdb7fef5f38e70a295818", size = 28237, upload-time = "2025-07-11T17:53:22.877Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/38/18c4bbe751a7357b3f6a33352e3af3305ad78f3e72ab7e3d667de4663ed9/urlobject-3.0.0-py3-none-any.whl", hash = "sha256:fd2465520d0a8c5ed983aa47518a2c5bcde0c276a4fd0eb28b0de5dcefd93b1e", size = 16261, upload-time = "2025-07-11T17:53:21.989Z" }, +] + +[[package]] +name = "uv" +version = "0.11.27" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/b8bb8bf2044561e3349ebe75be0d8abdeb19577886753b49200ec6caea0d/uv-0.11.27.tar.gz", hash = "sha256:3469204521869f0e6bdea17b02c1d86db2d0150820895653a6152cab206fb00b", size = 5978029, upload-time = "2026-07-06T21:00:29.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fe/8a080fb603f276ceb2d960454b2b5998cc6e5c0c6d5dbbbeb48b141de4f2/uv-0.11.27-py3-none-linux_armv6l.whl", hash = "sha256:6cedad2185b16bd5da77096d4813159c9c3c572897537256eecdb4092941adc0", size = 25490659, upload-time = "2026-07-06T20:59:29.003Z" }, + { url = "https://files.pythonhosted.org/packages/d1/15/886a2ef7b00c1a4fcf706e7a6b946b3f7485a55f42d0aa9b5572caed58b6/uv-0.11.27-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42b273459864c67f3736ae2a63929df463975d885bbc1afd8f53f7e0fb1f2269", size = 24498434, upload-time = "2026-07-06T20:59:33.042Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6e/45335717c4372bf955c87bc05c6462dd4c0c6087ed12c3991b14cbba77cd/uv-0.11.27-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b316464a1028a43478b74424b4b78925c463758baec93cb754fb2392ba175138", size = 23248727, upload-time = "2026-07-06T20:59:36.201Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fa/41dde1e350c5a277b797371faf96fb013c2c13ed18df6efc51ed8ec9bb5b/uv-0.11.27-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3ca478883d78270ceed57f229d1b480dbcadf5d79b2ce9624574807d6ec48554", size = 25208126, upload-time = "2026-07-06T20:59:39.984Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/bc6d4af24ab533db4c741cebade3a58b91fc0563059d39e838161dd210c8/uv-0.11.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:232dfb7549c8a4217ab1970dea0de731926b8ca21769ed89951ad8f8c3bce8b6", size = 25030974, upload-time = "2026-07-06T20:59:43.378Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0c/82e736930d3f78ca6c3286ef31627d7155e20191af88afc40ec29486f400/uv-0.11.27-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0cf7c7960587a231252cbe4d7a032b5a02c29cfab3fcc4e75a915b1c5cacee", size = 25015115, upload-time = "2026-07-06T20:59:46.694Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5b/512ebf3b24b708ea2adda4ec4d4bd23b2404d7a7ea3bd24d2b92210ab680/uv-0.11.27-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47b5863ef715ffe4c29e792c9884a4e8b8016b0628d3613ac84c696acc128a78", size = 26487914, upload-time = "2026-07-06T20:59:50.414Z" }, + { url = "https://files.pythonhosted.org/packages/98/d5/3871185331266156fbd09ab0ad7ddc69ca759b3a28ece5488bdb64f152d9/uv-0.11.27-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5764155d2a578a41d2827f464d4ce5b7788632e732cb304e2025fb3d043b46f", size = 27393306, upload-time = "2026-07-06T20:59:53.852Z" }, + { url = "https://files.pythonhosted.org/packages/24/0f/9fe3cc9b8d498f2e426b3cda90d337b076d64f19c2d55cb0b7d091c4bfe9/uv-0.11.27-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d806e1067c8eb9f8ba5dbdca46484512d224070e8473442319d4c1fbc3246f4d", size = 26563495, upload-time = "2026-07-06T20:59:56.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/18393c15ac0c982fefa34944135ead3adc565da5023d973eff5081a95a62/uv-0.11.27-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cb783b9db3868e51e8da1956aaa7e800262ad1af03dfc8b024f3b04ecb447c2", size = 26652374, upload-time = "2026-07-06T21:00:00.256Z" }, + { url = "https://files.pythonhosted.org/packages/b8/93/1bb8bee2b0bcc6e9655518ecaa9a792b1952dd99ae7366fe4f6c82754361/uv-0.11.27-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8e2b86d3dee515eeba6ea001d8b3eac20b204db07d39b04187c755a490a4fba6", size = 25341573, upload-time = "2026-07-06T21:00:03.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e0/fae0d1c972b25ac192ac807c1b1baf0f12070c40c8af2eb8e22310ea8846/uv-0.11.27-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:d325b8bd2665ce3ac58d33f977683cfb2cb8eb951effab06fad8518426bc0a90", size = 26051363, upload-time = "2026-07-06T21:00:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a2/4c0c103ea431f5d70cd33f10c754b5b1de9687fea982ca1a20a4eee0a4a7/uv-0.11.27-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d5c9036b558a323f254e3d16038183c22c55e0d06b921f1ed9706953311e52f", size = 26148896, upload-time = "2026-07-06T21:00:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/bb/b7/a514307a90ca9f1cb59efd0898958dea6c1092ad5e4e6b6f2eb4d55109db/uv-0.11.27-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4cc04e625e7bfac6975fd384d6598169cea0030995ff3b557950ec3077974a4d", size = 25701138, upload-time = "2026-07-06T21:00:13.772Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/c6cfa675d727a199cfce32f7a30350ce6e47f699c3cd7f0433a619c671d4/uv-0.11.27-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:109d8e145b390d0e14f2aefd9708a7522a4236d6bee20361a7558e77a14c0531", size = 26913232, upload-time = "2026-07-06T21:00:17.233Z" }, + { url = "https://files.pythonhosted.org/packages/a4/04/0070ecdea37baa9d38187abedc78eed9a52a2e6cb1d4f175c580640f3456/uv-0.11.27-py3-none-win32.whl", hash = "sha256:b330d2ab6af0f79396fb350a9fed8ed1aa92516310f7533de9a8dfebc4e147cd", size = 24374672, upload-time = "2026-07-06T21:00:20.399Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b9/f3df522d2e609fe511e5bee24b34c91ce281ce472594121fa1d5fa159d9d/uv-0.11.27-py3-none-win_amd64.whl", hash = "sha256:e20f25921ed4c46d32ac18a9b2e610c0384439fd367f647f2b68e0e1d90e9885", size = 27275452, upload-time = "2026-07-06T21:00:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/4ea9c3bbf95a21c627a3dfa359f5a3266e648cd103c5ade374a6e4265c5f/uv-0.11.27-py3-none-win_arm64.whl", hash = "sha256:a41da27667e95c0df939e99e77a4d9169fb895df0f6788b2ab65ed5efdc1395f", size = 25561477, upload-time = "2026-07-06T21:00:27.333Z" }, +] + +[[package]] +name = "vine" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/65/ec1d92091671e6407d3e7c1f5801413bb7b2b57630a50cae7750456ba0ed/virtualenv-21.6.0.tar.gz", hash = "sha256:e18a4d750f2b64dea73e72ffde3922f3c52365fabdc8157ebd3da20d031c4734", size = 5526111, upload-time = "2026-07-06T22:49:56.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/e7/2fbd0cc1653c53eed8f10670538bb547de2b3e37aacad283faa82a71094b/virtualenv-21.6.0-py3-none-any.whl", hash = "sha256:bce9d097950fef9d81129b333babfb7767072850c2f1acce0ec536708401bfd1", size = 5506216, upload-time = "2026-07-06T22:49:54.941Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, +]